From 19e2f432036279068521baae4e1572d34991947c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Fri, 4 Sep 2026 19:20:37 +0200 Subject: [PATCH 1/2] :bug: constant-time HMAC security fix --- .../com/mindee/parsing/BaseLocalResponse.java | 13 ++++- .../mindee/v1/parsing/LocalResponseTest.java | 2 + .../mindee/v2/parsing/LocalResponseTest.java | 48 ++++++++++++------- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/mindee/parsing/BaseLocalResponse.java b/src/main/java/com/mindee/parsing/BaseLocalResponse.java index 3284e9984..b25d96016 100644 --- a/src/main/java/com/mindee/parsing/BaseLocalResponse.java +++ b/src/main/java/com/mindee/parsing/BaseLocalResponse.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -99,10 +100,18 @@ public String getHmacSignature(String secretKey) { * Verify that the payload's signature matches the one received from the server. * * @param secretKey Your secret key from the Mindee platform. - * @param signature The signature from the "X-Mindee-Hmac-Signature" HTTP header. + * @param signature The signature from the "X-Signature" HTTP header. * @return true if the signatures match. */ public boolean isValidHmacSignature(String secretKey, String signature) { - return signature.equals(getHmacSignature(secretKey)); + if (signature == null || secretKey == null) { + return false; + } + byte[] expectedBytes = getHmacSignature(secretKey).getBytes(StandardCharsets.UTF_8); + byte[] actualBytes = signature + .toLowerCase(java.util.Locale.ROOT) + .getBytes(StandardCharsets.UTF_8); + + return MessageDigest.isEqual(expectedBytes, actualBytes); } } diff --git a/src/test/java/com/mindee/v1/parsing/LocalResponseTest.java b/src/test/java/com/mindee/v1/parsing/LocalResponseTest.java index 981fb194b..fbfb2d8f5 100644 --- a/src/test/java/com/mindee/v1/parsing/LocalResponseTest.java +++ b/src/test/java/com/mindee/v1/parsing/LocalResponseTest.java @@ -12,8 +12,10 @@ import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +@DisplayName("MindeeV1 – Load Local Response") public class LocalResponseTest { /** * Fake secret key. diff --git a/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java b/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java index ad59a14e3..37cdc316e 100644 --- a/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java +++ b/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java @@ -2,31 +2,40 @@ import static com.mindee.TestingUtilities.getResourcePath; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.mindee.MindeeException; import com.mindee.v2.product.extraction.ExtractionResponse; import java.io.IOException; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +@DisplayName("MindeeV2 – Load Local Response") public class LocalResponseTest { - @Test - void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException { - var localResponse = new LocalResponse( - getResourcePath("v2/products/extraction/financial_document/complete.json") - ); - ExtractionResponse loaded = localResponse.deserializeResponse(ExtractionResponse.class); + private static final String SIGNATURE = "79dd6572f8a97822fb12f2f72bc84ecdc7c968dede712cf23a256ac3eac593d4"; + private static final String DUMMY_SECRET_KEY = "ogNjY44MhvKPGTtVsI8zG82JqWQa68woYQH"; + + private static void assertLocalResponse(LocalResponse localResponse) { + assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, "invalid signature")); + assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, null)); + assertFalse(localResponse.isValidHmacSignature(null, SIGNATURE)); + assertFalse(localResponse.isValidHmacSignature(null, null)); + assertEquals(SIGNATURE, localResponse.getHmacSignature(DUMMY_SECRET_KEY)); + assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE)); - assertNotNull(loaded, "Loaded InferenceResponse must not be null"); + ExtractionResponse response = localResponse.deserializeResponse(ExtractionResponse.class); + assertNotNull(response, "Loaded ExtractionResponse must not be null"); assertEquals( "12345678-1234-1234-1234-123456789abc", - loaded.getInference().getModel().getId(), + response.getInference().getModel().getId(), "Model Id mismatch" ); assertEquals( "John Smith", - loaded + response .getInference() .getResult() .getFields() @@ -37,14 +46,21 @@ void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException { ); } + @Test + void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException { + var localResponse = new LocalResponse( + getResourcePath("v2/products/extraction/financial_document/complete.json") + ); + assertLocalResponse(localResponse); + } + @Test void givenInvalidJsonInput_shouldThrow() { var localResponse = new LocalResponse("{invalid json"); - var err = Assertions - .assertThrows( - MindeeException.class, - () -> localResponse.deserializeResponse(ExtractionResponse.class) - ); - Assertions.assertEquals("Invalid JSON payload.", err.getMessage()); + var err = assertThrows( + MindeeException.class, + () -> localResponse.deserializeResponse(ExtractionResponse.class) + ); + assertEquals("Invalid JSON payload.", err.getMessage()); } } From ccc0e9539f88b0963c6622be0370ffc79f7b44ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Fri, 4 Sep 2026 19:20:37 +0200 Subject: [PATCH 2/2] :bug: constant-time HMAC security fix --- src/main/java/com/mindee/parsing/BaseLocalResponse.java | 8 +++++++- .../java/com/mindee/v2/parsing/LocalResponseTest.java | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mindee/parsing/BaseLocalResponse.java b/src/main/java/com/mindee/parsing/BaseLocalResponse.java index b25d96016..6d5b5e14d 100644 --- a/src/main/java/com/mindee/parsing/BaseLocalResponse.java +++ b/src/main/java/com/mindee/parsing/BaseLocalResponse.java @@ -107,7 +107,13 @@ public boolean isValidHmacSignature(String secretKey, String signature) { if (signature == null || secretKey == null) { return false; } - byte[] expectedBytes = getHmacSignature(secretKey).getBytes(StandardCharsets.UTF_8); + + String expectedSignature = getHmacSignature(secretKey); + if (expectedSignature.isEmpty()) { + return false; + } + + byte[] expectedBytes = expectedSignature.getBytes(StandardCharsets.UTF_8); byte[] actualBytes = signature .toLowerCase(java.util.Locale.ROOT) .getBytes(StandardCharsets.UTF_8); diff --git a/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java b/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java index 37cdc316e..19d2cc8fb 100644 --- a/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java +++ b/src/test/java/com/mindee/v2/parsing/LocalResponseTest.java @@ -19,12 +19,15 @@ public class LocalResponseTest { private static final String DUMMY_SECRET_KEY = "ogNjY44MhvKPGTtVsI8zG82JqWQa68woYQH"; private static void assertLocalResponse(LocalResponse localResponse) { + assertEquals(SIGNATURE, localResponse.getHmacSignature(DUMMY_SECRET_KEY)); + assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, "invalid signature")); assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, null)); assertFalse(localResponse.isValidHmacSignature(null, SIGNATURE)); assertFalse(localResponse.isValidHmacSignature(null, null)); - assertEquals(SIGNATURE, localResponse.getHmacSignature(DUMMY_SECRET_KEY)); + assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, "")); assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE)); + assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE.toUpperCase())); ExtractionResponse response = localResponse.deserializeResponse(ExtractionResponse.class); assertNotNull(response, "Loaded ExtractionResponse must not be null");