Skip to content

Commit 8a437ef

Browse files
authored
✨ add HMAC validation for webhooks (#162)
1 parent a217329 commit 8a437ef

File tree

5 files changed

+140
-13
lines changed

5 files changed

+140
-13
lines changed

src/main/java/com/mindee/CommandLineInterface.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.mindee.parsing.common.Inference;
1010
import com.mindee.parsing.common.PredictResponse;
1111
import com.mindee.product.custom.CustomV1;
12+
import com.mindee.product.internationalid.InternationalIdV2;
1213
import com.mindee.product.invoice.InvoiceV4;
1314
import com.mindee.product.invoicesplitter.InvoiceSplitterV1;
1415
import com.mindee.product.multireceiptsdetector.MultiReceiptsDetectorV1;
@@ -80,46 +81,54 @@ public static void main(String[] args) {
8081
System.exit(exitCode);
8182
}
8283

83-
@Command(name = "invoice", description = "Invokes the Invoice API")
84+
@Command(name = "invoice", description = "Parse using Invoice")
8485
void invoiceMethod(
8586
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
8687
File file
8788
) throws IOException {
8889
System.out.println(standardProductOutput(InvoiceV4.class, file));
8990
}
9091

91-
@Command(name = "receipt", description = "Invokes the Expense Receipt API")
92+
@Command(name = "receipt", description = "Parse using Expense Receipt")
9293
void receiptMethod(
9394
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
9495
File file
9596
) throws IOException {
9697
System.out.println(standardProductOutput(ReceiptV4.class, file));
9798
}
9899

99-
@Command(name = "multi-receipt-detector", description = "Invokes the Multi Receipts Detector API")
100+
@Command(name = "multi-receipt-detector", description = "Parse using Multi Receipts Detector")
100101
void multiReceiptDetectorMethod(
101102
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
102103
File file
103104
) throws IOException {
104105
System.out.println(standardProductOutput(MultiReceiptsDetectorV1.class, file));
105106
}
106107

107-
@Command(name = "passport", description = "Invokes the Passport API")
108+
@Command(name = "passport", description = "Parse using Passport")
108109
void passportMethod(
109110
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
110111
File file
111112
) throws IOException {
112113
System.out.println(standardProductOutput(PassportV1.class, file));
113114
}
114115

115-
@Command(name = "invoice-splitter", description = "Invokes the Invoice Splitter API")
116+
@Command(name = "invoice-splitter", description = "Parse using Invoice Splitter")
116117
void invoiceSplitterMethod(
117118
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
118119
File file
119120
) throws IOException, InterruptedException {
120121
System.out.println(standardProductAsyncOutput(InvoiceSplitterV1.class, file));
121122
}
122123

124+
@Command(name = "international-id", description = "Parse using International ID")
125+
void internationalIdMethod(
126+
@Parameters(index = "0", paramLabel = "<path>", scope = ScopeType.LOCAL)
127+
File file
128+
) throws IOException, InterruptedException {
129+
System.out.println(standardProductAsyncOutput(InternationalIdV2.class, file));
130+
}
131+
123132
@Command(name = "custom", description = "Invokes a Custom API")
124133
void customMethod(
125134
@Option(
Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package com.mindee.input;
22

3+
import java.io.BufferedReader;
34
import java.io.File;
45
import java.io.IOException;
56
import java.io.InputStream;
7+
import java.io.InputStreamReader;
68
import java.nio.charset.StandardCharsets;
79
import java.nio.file.Files;
10+
import java.security.InvalidKeyException;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.util.stream.Collectors;
13+
import java.util.stream.Stream;
14+
import javax.crypto.Mac;
15+
import javax.crypto.spec.SecretKeySpec;
816
import lombok.Getter;
9-
import org.apache.pdfbox.io.IOUtils;
17+
import org.apache.commons.codec.binary.Hex;
1018

1119
/**
1220
* A Mindee response saved locally.
@@ -17,22 +25,70 @@ public class LocalResponse {
1725

1826
/**
1927
* Load from an {@link InputStream}.
28+
* @param input will be decoded as UTF-8.
2029
*/
2130
public LocalResponse(InputStream input) throws IOException {
22-
this.file = IOUtils.toByteArray(input);
31+
this.file = this.getBytes(
32+
new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))
33+
.lines()
34+
);
2335
}
2436

2537
/**
26-
* Load from a UTF-8 {@link String}.
38+
* Load from a {@link String}.
39+
* @param input will be decoded as UTF-8.
2740
*/
2841
public LocalResponse(String input) {
2942
this.file = input.getBytes(StandardCharsets.UTF_8);
3043
}
3144

3245
/**
3346
* Load from a {@link File}.
47+
* @param input will be decoded as UTF-8.
3448
*/
3549
public LocalResponse(File input) throws IOException {
36-
this.file = Files.readAllBytes(input.toPath());
50+
this.file = this.getBytes(
51+
Files.lines(input.toPath(), StandardCharsets.UTF_8)
52+
);
53+
}
54+
55+
private byte[] getBytes(Stream<String> stream) {
56+
return stream.collect(Collectors.joining("")).getBytes();
57+
}
58+
59+
/**
60+
* Get the HMAC signature of the payload.
61+
* @param secretKey Your secret key from the Mindee platform.
62+
* @return The generated HMAC signature.
63+
*/
64+
public String getHmacSignature(String secretKey) {
65+
String algorithm = "HmacSHA256";
66+
SecretKeySpec secretKeySpec = new SecretKeySpec(
67+
secretKey.getBytes(StandardCharsets.UTF_8),
68+
algorithm
69+
);
70+
Mac mac;
71+
try {
72+
mac = Mac.getInstance(algorithm);
73+
} catch (NoSuchAlgorithmException err) {
74+
// this should never happen as the algorithm is hard-coded.
75+
return "";
76+
}
77+
try {
78+
mac.init(secretKeySpec);
79+
} catch (InvalidKeyException err) {
80+
return "";
81+
}
82+
return Hex.encodeHexString(mac.doFinal(this.file));
83+
}
84+
85+
/**
86+
* Verify that the payload's signature matches the one received from the server.
87+
* @param secretKey Your secret key from the Mindee platform.
88+
* @param signature The signature from the "X-Mindee-Hmac-Signature" HTTP header.
89+
* @return true if the signatures match.
90+
*/
91+
public boolean isValidHmacSignature(String secretKey, String signature) {
92+
return signature.equals(getHmacSignature(secretKey));
3793
}
3894
}

src/test/java/com/mindee/input/LocalInputSourceTest.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ void loadDocument_withInputStream_mustReturnAValidLocalInputSource() throws IOEx
2121
File file = new File("src/test/resources/file_types/pdf/multipage.pdf");
2222
LocalInputSource localInputSource = new LocalInputSource(
2323
Files.newInputStream(file.toPath()),
24-
"multipage.pdf");
24+
"multipage.pdf"
25+
);
2526
Assertions.assertNotNull(localInputSource);
2627
Assertions.assertArrayEquals(localInputSource.getFile(), Files.readAllBytes(file.toPath()));
2728
}
@@ -31,7 +32,8 @@ void loadDocument_withByteArray_mustReturnAValidLocalInputSource() throws IOExce
3132
File file = new File("src/test/resources/file_types/pdf/multipage.pdf");
3233
LocalInputSource localInputSource = new LocalInputSource(
3334
Files.readAllBytes(file.toPath()),
34-
"multipage.pdf");
35+
"multipage.pdf"
36+
);
3537
Assertions.assertNotNull(localInputSource);
3638
Assertions.assertArrayEquals(localInputSource.getFile(), Files.readAllBytes(file.toPath()));
3739
}
@@ -42,7 +44,8 @@ void loadDocument_withBase64Encoded_mustReturnAValidLocalInputSource() throws IO
4244
String encodedFile = Base64.encodeBase64String(Files.readAllBytes(file.toPath()));
4345
LocalInputSource localInputSource = new LocalInputSource(
4446
encodedFile,
45-
"multipage.pdf");
47+
"multipage.pdf"
48+
);
4649
Assertions.assertNotNull(localInputSource);
4750
Assertions.assertArrayEquals(localInputSource.getFile(), Files.readAllBytes(file.toPath()));
4851
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.mindee.input;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
import java.io.File;
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Paths;
9+
10+
11+
public class LocalResponseTest {
12+
/**
13+
* Fake secret key.
14+
*/
15+
String secretKey = "ogNjY44MhvKPGTtVsI8zG82JqWQa68woYQH";
16+
17+
/**
18+
* Real signature using fake secret key.
19+
*/
20+
String signature = "5ed1673e34421217a5dbfcad905ee62261a3dd66c442f3edd19302072bbf70d0";
21+
22+
/**
23+
* File which the signature applies to.
24+
*/
25+
String filePath = "src/test/resources/async/get_completed_empty.json";
26+
27+
@Test
28+
void loadDocument_withFile_mustReturnValidLocalResponse() throws IOException {
29+
LocalResponse localResponse = new LocalResponse(new File(this.filePath));
30+
Assertions.assertNotNull(localResponse.getFile());
31+
Assertions.assertFalse(localResponse.isValidHmacSignature(
32+
this.secretKey, "invalid signature is invalid")
33+
);
34+
Assertions.assertEquals(this.signature, localResponse.getHmacSignature(this.secretKey));
35+
Assertions.assertTrue(localResponse.isValidHmacSignature(this.secretKey, this.signature));
36+
}
37+
38+
@Test
39+
void loadDocument_withString_mustReturnValidLocalResponse() {
40+
LocalResponse localResponse = new LocalResponse("{'some': 'json', 'with': 'data'}");
41+
Assertions.assertNotNull(localResponse.getFile());
42+
Assertions.assertFalse(localResponse.isValidHmacSignature(
43+
this.secretKey, "invalid signature is invalid")
44+
);
45+
}
46+
47+
@Test
48+
void loadDocument_withInputStream_mustReturnValidLocalResponse() throws IOException {
49+
LocalResponse localResponse = new LocalResponse(
50+
Files.newInputStream(Paths.get(this.filePath))
51+
);
52+
Assertions.assertNotNull(localResponse.getFile());
53+
Assertions.assertFalse(localResponse.isValidHmacSignature(
54+
this.secretKey, "invalid signature is invalid")
55+
);
56+
Assertions.assertEquals(this.signature, localResponse.getHmacSignature(this.secretKey));
57+
Assertions.assertTrue(localResponse.isValidHmacSignature(this.secretKey, this.signature));
58+
}
59+
}

0 commit comments

Comments
 (0)