Skip to content

Commit ddd141e

Browse files
committed
🐛 constant-time HMAC security fix
1 parent 5615c0a commit ddd141e

3 files changed

Lines changed: 45 additions & 18 deletions

File tree

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

Lines changed: 11 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,18 @@ 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+
byte[] expectedBytes = getHmacSignature(secretKey).getBytes(StandardCharsets.UTF_8);
111+
byte[] actualBytes = signature
112+
.toLowerCase(java.util.Locale.ROOT)
113+
.getBytes(StandardCharsets.UTF_8);
114+
115+
return MessageDigest.isEqual(expectedBytes, actualBytes);
107116
}
108117
}

‎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: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,40 @@
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+
assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, "invalid signature"));
23+
assertFalse(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, null));
24+
assertFalse(localResponse.isValidHmacSignature(null, SIGNATURE));
25+
assertFalse(localResponse.isValidHmacSignature(null, null));
26+
assertEquals(SIGNATURE, localResponse.getHmacSignature(DUMMY_SECRET_KEY));
27+
assertTrue(localResponse.isValidHmacSignature(DUMMY_SECRET_KEY, SIGNATURE));
2028

21-
assertNotNull(loaded, "Loaded InferenceResponse must not be null");
29+
ExtractionResponse response = localResponse.deserializeResponse(ExtractionResponse.class);
30+
assertNotNull(localResponse, "Loaded InferenceResponse must not be null");
2231
assertEquals(
2332
"12345678-1234-1234-1234-123456789abc",
24-
loaded.getInference().getModel().getId(),
33+
response.getInference().getModel().getId(),
2534
"Model Id mismatch"
2635
);
2736
assertEquals(
2837
"John Smith",
29-
loaded
38+
response
3039
.getInference()
3140
.getResult()
3241
.getFields()
@@ -37,14 +46,21 @@ void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException {
3746
);
3847
}
3948

49+
@Test
50+
void loadDocument_withPath_mustReturnValidLocalResponse() throws IOException {
51+
var localResponse = new LocalResponse(
52+
getResourcePath("v2/products/extraction/financial_document/complete.json")
53+
);
54+
AssertLocalResponse(localResponse);
55+
}
56+
4057
@Test
4158
void givenInvalidJsonInput_shouldThrow() {
4259
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());
60+
var err = assertThrows(
61+
MindeeException.class,
62+
() -> localResponse.deserializeResponse(ExtractionResponse.class)
63+
);
64+
assertEquals("Invalid JSON payload.", err.getMessage());
4965
}
5066
}

0 commit comments

Comments
 (0)