Skip to content

Commit c17d58e

Browse files
authored
🐛 constant-time HMAC security fix (#353)
1 parent 5615c0a commit c17d58e

3 files changed

Lines changed: 54 additions & 18 deletions

File tree

src/main/java/com/mindee/parsing/BaseLocalResponse.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.nio.file.Files;
1010
import java.nio.file.Path;
1111
import java.security.InvalidKeyException;
12+
import java.security.MessageDigest;
1213
import java.security.NoSuchAlgorithmException;
1314
import java.util.stream.Collectors;
1415
import java.util.stream.Stream;
@@ -99,10 +100,24 @@ public String getHmacSignature(String secretKey) {
99100
* Verify that the payload's signature matches the one received from the server.
100101
*
101102
* @param secretKey Your secret key from the Mindee platform.
102-
* @param signature The signature from the "X-Mindee-Hmac-Signature" HTTP header.
103+
* @param signature The signature from the "X-Signature" HTTP header.
103104
* @return true if the signatures match.
104105
*/
105106
public boolean isValidHmacSignature(String secretKey, String signature) {
106-
return signature.equals(getHmacSignature(secretKey));
107+
if (signature == null || secretKey == null) {
108+
return false;
109+
}
110+
111+
String expectedSignature = getHmacSignature(secretKey);
112+
if (expectedSignature.isEmpty()) {
113+
return false;
114+
}
115+
116+
byte[] expectedBytes = expectedSignature.getBytes(StandardCharsets.UTF_8);
117+
byte[] actualBytes = signature
118+
.toLowerCase(java.util.Locale.ROOT)
119+
.getBytes(StandardCharsets.UTF_8);
120+
121+
return MessageDigest.isEqual(expectedBytes, actualBytes);
107122
}
108123
}

src/test/java/com/mindee/v1/parsing/LocalResponseTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import java.nio.file.Files;
1313
import java.nio.file.Path;
1414
import org.junit.jupiter.api.Assertions;
15+
import org.junit.jupiter.api.DisplayName;
1516
import org.junit.jupiter.api.Test;
1617

18+
@DisplayName("MindeeV1 – Load Local Response")
1719
public class LocalResponseTest {
1820
/**
1921
* Fake secret key.

src/test/java/com/mindee/v2/parsing/LocalResponseTest.java

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,43 @@
22

33
import static com.mindee.TestingUtilities.getResourcePath;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
56
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
69

710
import com.mindee.MindeeException;
811
import com.mindee.v2.product.extraction.ExtractionResponse;
912
import java.io.IOException;
10-
import org.junit.jupiter.api.Assertions;
13+
import org.junit.jupiter.api.DisplayName;
1114
import org.junit.jupiter.api.Test;
1215

16+
@DisplayName("MindeeV2 – Load Local Response")
1317
public class LocalResponseTest {
14-
@Test
15-
void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException {
16-
var localResponse = new LocalResponse(
17-
getResourcePath("v2/products/extraction/financial_document/complete.json")
18-
);
19-
ExtractionResponse loaded = localResponse.deserializeResponse(ExtractionResponse.class);
18+
private static final String SIGNATURE = "79dd6572f8a97822fb12f2f72bc84ecdc7c968dede712cf23a256ac3eac593d4";
19+
private static final String DUMMY_SECRET_KEY = "ogNjY44MhvKPGTtVsI8zG82JqWQa68woYQH";
20+
21+
private static void assertLocalResponse(LocalResponse localResponse) {
22+
assertEquals(SIGNATURE, localResponse.getHmacSignature(DUMMY_SECRET_KEY));
2023

21-
assertNotNull(loaded, "Loaded InferenceResponse must not be null");
24+
assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, "invalid signature"));
25+
assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, null));
26+
assertFalse(localResponse.isValidHmacSignature(null, SIGNATURE));
27+
assertFalse(localResponse.isValidHmacSignature(null, null));
28+
assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, ""));
29+
assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE));
30+
assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE.toUpperCase()));
31+
32+
ExtractionResponse response = localResponse.deserializeResponse(ExtractionResponse.class);
33+
assertNotNull(response, "Loaded ExtractionResponse must not be null");
2234
assertEquals(
2335
"12345678-1234-1234-1234-123456789abc",
24-
loaded.getInference().getModel().getId(),
36+
response.getInference().getModel().getId(),
2537
"Model Id mismatch"
2638
);
2739
assertEquals(
2840
"John Smith",
29-
loaded
41+
response
3042
.getInference()
3143
.getResult()
3244
.getFields()
@@ -37,14 +49,21 @@ void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException {
3749
);
3850
}
3951

52+
@Test
53+
void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException {
54+
var localResponse = new LocalResponse(
55+
getResourcePath("v2/products/extraction/financial_document/complete.json")
56+
);
57+
assertLocalResponse(localResponse);
58+
}
59+
4060
@Test
4161
void givenInvalidJsonInput_shouldThrow() {
4262
var localResponse = new LocalResponse("{invalid json");
43-
var err = Assertions
44-
.assertThrows(
45-
MindeeException.class,
46-
() -> localResponse.deserializeResponse(ExtractionResponse.class)
47-
);
48-
Assertions.assertEquals("Invalid JSON payload.", err.getMessage());
63+
var err = assertThrows(
64+
MindeeException.class,
65+
() -> localResponse.deserializeResponse(ExtractionResponse.class)
66+
);
67+
assertEquals("Invalid JSON payload.", err.getMessage());
4968
}
5069
}

0 commit comments

Comments
 (0)