Skip to content

Commit faff595

Browse files
authored
Resolve VCSWP-19101 (#34)
Add signing secret verification util classes
1 parent 2ccdc53 commit faff595

File tree

11 files changed

+425
-9
lines changed

11 files changed

+425
-9
lines changed

.openapi-generator/FILES

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,3 @@ src/main/java/com/github/freeclimbapi/auth/ApiKeyAuth.java
286286
src/main/java/com/github/freeclimbapi/auth/Authentication.java
287287
src/main/java/com/github/freeclimbapi/auth/HttpBasicAuth.java
288288
src/main/java/com/github/freeclimbapi/auth/HttpBearerAuth.java
289-
src/test/java/com/github/freeclimbapi/DefaultApiTest.java

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
99

1010
None
1111

12+
<a name="5.2.0"></a>
13+
14+
## [5.2.0] 2023-04-03
15+
16+
### Added
17+
18+
- Introduce signing secret verification class (RequestVerifier) - https://docs.freeclimb.com/docs/validating-requests-from-freeclimb#how-to-verify-requests-manually
19+
1220
<a name="5.1.3"></a>
1321

1422
## [5.1.3] 2023-03-13

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Add this dependency to your project's POM:
4040
<dependency>
4141
<groupId>com.github.freeclimbapi</groupId>
4242
<artifactId>freeclimb-java-client</artifactId>
43-
<version>5.1.3</version>
43+
<version>5.2.0</version>
4444
<scope>compile</scope>
4545
</dependency>
4646
```
@@ -56,7 +56,7 @@ Add this dependency to your project's build file:
5656
}
5757
5858
dependencies {
59-
implementation "com.github.freeclimbapi:freeclimb-java-client:5.1.3"
59+
implementation "com.github.freeclimbapi:freeclimb-java-client:5.2.0"
6060
}
6161
```
6262

@@ -70,7 +70,7 @@ mvn clean package
7070

7171
Then manually install the following JARs:
7272

73-
* `target/freeclimb-java-client-5.1.3.jar`
73+
* `target/freeclimb-java-client-5.2.0.jar`
7474
* `target/lib/*.jar`
7575

7676
## Getting Started
@@ -328,6 +328,37 @@ Authentication schemes defined for the API:
328328
329329
- **Type**: HTTP basic authentication
330330
331+
<a name="documentation-for-verify-request-signature"></a>
332+
333+
## Documentation for verifying request signature
334+
335+
- To verify the request signature, we will need to use the verifyRequestSignature method within the Request Verifier class
336+
337+
RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance)
338+
339+
This is a method that you can call directly from the request verifier class, it will throw exceptions depending on whether all parts of the request signature is valid otherwise it will throw a specific error message depending on which request signature part is causing issues
340+
341+
This method requires a requestBody of type String, a requestHeader of type String, a signingSecret of type String, and a tolerance value of type Integer
342+
343+
Example code down below
344+
345+
```java
346+
package com.github.freeclimbapi;
347+
348+
import com.github.freeclimbapi.utils.*;
349+
import java.security.NoSuchAlgorithmException;
350+
import java.security.InvalidKeyException;
351+
352+
public class Example {
353+
public void verifyRequestSignatureExample() throws NoSuchAlgorithmException, InvalidKeyException {
354+
String requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}";
355+
String signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793";
356+
String requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8";
357+
Integer tolerance = 5 * 60;
358+
RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance);
359+
}
360+
}
361+
```
331362
332363
## Recommendation
333364
@@ -336,4 +367,3 @@ It's recommended to create an instance of `ApiClient` per thread in a multithrea
336367
## Author
337368
338369
339-

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ apply plugin: 'java'
44
apply plugin: 'com.diffplug.spotless'
55

66
group = 'com.github.freeclimbapi'
7-
version = '5.1.3'
7+
version = '5.2.0'
88

99
buildscript {
1010
repositories {
@@ -114,6 +114,7 @@ dependencies {
114114
implementation 'io.gsonfire:gson-fire:1.8.4'
115115
implementation 'org.openapitools:jackson-databind-nullable:0.2.1'
116116
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10'
117+
implementation group: 'commons-codec', name: 'commons-codec', version: '1.7'
117118
implementation 'org.threeten:threetenbp:1.4.3'
118119
implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version"
119120
testImplementation 'junit:junit:4.13.1'

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ lazy val root = (project in file(".")).
22
settings(
33
organization := "com.github.freeclimbapi",
44
name := "freeclimb-java-client",
5-
version := "5.1.3",
5+
version := "5.2.0",
66
scalaVersion := "2.11.4",
77
scalacOptions ++= Seq("-feature"),
88
javacOptions in compile ++= Seq("-Xlint:deprecation"),

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<artifactId>freeclimb-java-client</artifactId>
66
<packaging>jar</packaging>
77
<name>freeclimb-java-client</name>
8-
<version>5.1.3</version>
8+
<version>5.2.0</version>
99
<url>https://github.com/freeclimbapi/java-sdk</url>
1010
<description>FreeClimb Java Client</description>
1111
<scm>

src/main/java/com/github/freeclimbapi/ApiClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private void init() {
131131
json = new JSON();
132132

133133
// Set default User-Agent.
134-
setUserAgent("OpenAPI-Generator/5.1.3/java");
134+
setUserAgent("OpenAPI-Generator/5.2.0/java");
135135

136136
authentications = new HashMap<String, Authentication>();
137137
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.github.freeclimbapi.utils;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.InvalidKeyException;
6+
import java.net.URLDecoder;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.ArrayList;
10+
import java.util.Date;
11+
12+
import javax.crypto.Mac;
13+
import javax.crypto.spec.SecretKeySpec;
14+
import org.apache.commons.codec.binary.Hex;
15+
16+
public class RequestVerifier {
17+
public static final Integer DEFAULT_TOLERANCE = 5 * 60 * 1000;
18+
19+
public static void verifyRequestSignature(String requestBody, String requestHeader, String signingSecret,
20+
Integer tolerance) throws NoSuchAlgorithmException, InvalidKeyException {
21+
RequestVerifier verifier = new RequestVerifier();
22+
verifier.checkRequestBody(requestBody);
23+
verifier.checkRequestHeader(requestHeader);
24+
verifier.checkSigningSecret(signingSecret);
25+
verifier.checkTolerance(tolerance);
26+
SignatureInformation info = new SignatureInformation(requestHeader);
27+
verifier.verifyTolerance(info, tolerance);
28+
verifier.verifySignature(info, requestBody, signingSecret);
29+
}
30+
31+
private void verifyRequestSignature(String requestBody, String requestHeader, String signingSecret)
32+
throws NoSuchAlgorithmException, InvalidKeyException {
33+
verifyRequestSignature(requestBody, requestHeader, signingSecret, DEFAULT_TOLERANCE);
34+
}
35+
36+
private void checkRequestBody(String requestBody) {
37+
if (requestBody == "" || requestBody == null) {
38+
throw new java.lang.RuntimeException("Request Body cannot be empty or null");
39+
}
40+
}
41+
42+
private void checkRequestHeader(String requestHeader) {
43+
if (requestHeader == "" || requestHeader == null) {
44+
throw new java.lang.RuntimeException("Error with request header, Request header is empty");
45+
} else if (!requestHeader.contains("t=")) {
46+
throw new java.lang.RuntimeException("Error with request header, timestamp is not present");
47+
} else if (!requestHeader.contains("v1=")) {
48+
throw new java.lang.RuntimeException("Error with request header, signatures are not present");
49+
}
50+
}
51+
52+
private void checkSigningSecret(String signingSecret) {
53+
if (signingSecret.equals("") || signingSecret.equals(null)) {
54+
throw new java.lang.RuntimeException("Signing secret cannot be empty or null");
55+
}
56+
}
57+
58+
private void checkTolerance(Integer tolerance) {
59+
if ((tolerance <= 0) || tolerance >= Integer.MAX_VALUE) {
60+
throw new java.lang.RuntimeException("Tolerance value must be a positive integer");
61+
}
62+
}
63+
64+
private void verifyTolerance(SignatureInformation info, Integer tolerance) {
65+
int currentTime = info.getCurrentUnixTime();
66+
if (!info.isRequestTimeValid(tolerance)) {
67+
throw new java.lang.RuntimeException(
68+
"Request time exceeded tolerance threshold. Request: " + info.requestTimestamp
69+
+ ", CurrentTime: " + Integer.toString(currentTime) + ", tolerance: " + tolerance);
70+
}
71+
}
72+
73+
private void verifySignature(SignatureInformation info, String requestBody, String signingSecret)
74+
throws NoSuchAlgorithmException, InvalidKeyException {
75+
if (!info.isSignatureSafe(requestBody, signingSecret)) {
76+
throw new java.lang.RuntimeException(
77+
"Unverified signature request, If this request was unexpected, it may be from a bad actor. Please proceed with caution. If the request was exepected, please check any typos or issues with the signingSecret");
78+
}
79+
}
80+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.github.freeclimbapi.utils;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.net.URLDecoder;
5+
import java.security.NoSuchAlgorithmException;
6+
import java.security.InvalidKeyException;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.ArrayList;
10+
import java.util.Date;
11+
12+
import javax.crypto.Mac;
13+
import javax.crypto.spec.SecretKeySpec;
14+
import org.apache.commons.codec.binary.Hex;
15+
16+
public class SignatureInformation {
17+
public Integer requestTimestamp;
18+
public List<String> signatures;
19+
20+
public SignatureInformation(String requestHeader) {
21+
signatures = new ArrayList<String>();
22+
String[] signatureHeaders = requestHeader.split(",");
23+
for (String signatureHeader : signatureHeaders) {
24+
String[] split = signatureHeader.split("=");
25+
String header = split[0];
26+
String value = split[1];
27+
if (header.equals("t")) {
28+
requestTimestamp = Integer.valueOf(value);
29+
} else if (header.equals("v1")) {
30+
signatures.add(value);
31+
}
32+
}
33+
}
34+
35+
public boolean isRequestTimeValid(Integer tolerance) {
36+
Integer currentUnixTimestamp = getCurrentUnixTime();
37+
return (requestTimestamp + tolerance) < currentUnixTimestamp;
38+
}
39+
40+
public boolean isSignatureSafe(String requestBody, String signingSecret)
41+
throws NoSuchAlgorithmException, InvalidKeyException {
42+
String hashValue = computeHash(requestBody, signingSecret);
43+
return signatures.contains(hashValue);
44+
}
45+
46+
private String computeHash(String requestBody, String signingSecret)
47+
throws NoSuchAlgorithmException, InvalidKeyException {
48+
String hashHexadecimalValue = "";
49+
String hashSeedString = requestTimestamp + "." + requestBody;
50+
Mac mac = Mac.getInstance("HmacSHA256");
51+
mac.init(new SecretKeySpec(signingSecret.getBytes(), "HmacSHA256"));
52+
return Hex.encodeHexString(mac.doFinal(hashSeedString.getBytes()));
53+
}
54+
55+
public Integer getCurrentUnixTime() {
56+
Long timeCalculation = (System.currentTimeMillis() / 1000L);
57+
return Integer.valueOf(timeCalculation.intValue());
58+
}
59+
}

0 commit comments

Comments
 (0)