Skip to content

Commit

Permalink
feat: add method for decoding sealed results
Browse files Browse the repository at this point in the history
  • Loading branch information
TheUnderScorer committed Jan 24, 2024
1 parent 2334468 commit 04986dc
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 33 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,43 @@ public class FingerprintApiExample {
}
```

## Sealed results

This SDK provides utility methods for decoding [sealed results](https://dev.fingerprint.com/docs/sealed-client-results).
```java
package com.fingerprint.example;

import com.fingerprint.Sealed;
import com.fingerprint.model.EventResponse;

import java.util.Base64;

public class SealedResults {
public static void main(String... args) throws Exception {
// Sealed result from the frontend.
String SEALED_RESULT = System.getenv("BASE64_SEALED_RESULT");

// Base64 encoded key generated in the dashboard.
String SEALED_KEY = System.getenv("BASE64_KEY");

final EventResponse event = Sealed.unsealEventResponse(
Base64.getDecoder().decode(SEALED_RESULT),
// You can provide more than one key to support key rotation. The SDK will try to decrypt the result with each key.
new Sealed.DecryptionKey[]{
new Sealed.DecryptionKey(
Base64.getDecoder().decode(SEALED_KEY),
Sealed.DecryptionAlgorithm.AES_256_GCM
)
}
);

// Do something with unsealed response, e.g: send it back to the frontend.
}
}

```
To learn more, refer to example located in [src/examples/java/com/fingerprint/example/SealedResults.java](src/examples/java/com/fingerprint/example/SealedResults.java).

## Documentation for API Endpoints

All URIs are relative to *https://api.fpjs.io*
Expand Down
27 changes: 27 additions & 0 deletions src/examples/java/com/fingerprint/example/EnvUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.fingerprint.example;

import io.github.cdimascio.dotenv.Dotenv;

import java.io.File;

public class EnvUtil {
private Dotenv dotenv;

public EnvUtil() {
// Load variables from .env if present, host environment variables still take precedence if present
File envFile = new File(".env");
if (envFile.exists()) {
dotenv = Dotenv.configure().load();
} else {
System.out.println(".env file not found. Skipping dotenv loading.");
}
}

public String getEnv(String key) {
String value = System.getenv(key);
if (value == null && dotenv != null) {
value = dotenv.get(key);
}
return value;
}
}
38 changes: 5 additions & 33 deletions src/examples/java/com/fingerprint/example/FunctionalTests.java
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
package com.fingerprint.example;

import com.fingerprint.api.FingerprintApi;
import com.fingerprint.model.EventResponse;
import com.fingerprint.model.Response;
import com.fingerprint.sdk.ApiClient;
import com.fingerprint.sdk.ApiException;
import com.fingerprint.sdk.Configuration;
import io.github.cdimascio.dotenv.Dotenv;
import java.io.File;

public class FunctionalTests {
public static void main(String... args) {
EnvUtil envUtil = new EnvUtil();

// Load variables from .env if present, host environment variables still take precedence if present
File envFile = new File(".env");
Dotenv dotenv = null;
if (envFile.exists()) {
dotenv = Dotenv.configure().load();
} else {
System.out.println(".env file not found. Skipping dotenv loading.");
}

String FPJS_API_SECRET = System.getenv("FPJS_API_SECRET");
if (FPJS_API_SECRET == null && dotenv != null) {
FPJS_API_SECRET = dotenv.get("FPJS_API_SECRET");
}

String FPJS_VISITOR_ID = System.getenv("FPJS_VISITOR_ID");
if (FPJS_VISITOR_ID == null && dotenv != null) {
FPJS_VISITOR_ID = dotenv.get("FPJS_VISITOR_ID");
}

String FPJS_REQUEST_ID = System.getenv("FPJS_REQUEST_ID");
if (FPJS_REQUEST_ID == null && dotenv != null) {
FPJS_REQUEST_ID = dotenv.get("FPJS_REQUEST_ID");
}

String FPJS_API_REGION = System.getenv("FPJS_API_REGION");
if (FPJS_API_REGION == null && dotenv != null) {
FPJS_API_REGION = dotenv.get("FPJS_API_REGION");
}

String FPJS_API_SECRET = envUtil.getEnv("FPJS_API_SECRET");
String FPJS_VISITOR_ID = envUtil.getEnv("FPJS_VISITOR_ID");
String FPJS_REQUEST_ID = envUtil.getEnv("FPJS_REQUEST_ID");
String FPJS_API_REGION = envUtil.getEnv("FPJS_API_REGION");

// Create a new instance of the API client
ApiClient client = Configuration.getDefaultApiClient(FPJS_API_SECRET, FPJS_API_REGION != null ? FPJS_API_REGION : "us");
Expand Down
28 changes: 28 additions & 0 deletions src/examples/java/com/fingerprint/example/SealedResults.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.fingerprint.example;

import com.fingerprint.Sealed;
import com.fingerprint.model.EventResponse;

import java.util.Base64;

public class SealedResults {
public static void main(String... args) throws Exception {
EnvUtil envUtil = new EnvUtil();

String SEALED_RESULT = envUtil.getEnv("BASE64_SEALED_RESULT");
String SEALED_KEY = envUtil.getEnv("BASE64_KEY");

final EventResponse event = Sealed.unsealEventResponse(
Base64.getDecoder().decode(SEALED_RESULT),
new Sealed.DecryptionKey[]{
new Sealed.DecryptionKey(
Base64.getDecoder().decode(SEALED_KEY),
Sealed.DecryptionAlgorithm.AES_256_GCM
)
}
);

System.out.println(event);
System.exit(0);
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/fingerprint/ObjectMapperUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.fingerprint;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

public class ObjectMapperUtil {
public static ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());

return mapper;
}
}
112 changes: 112 additions & 0 deletions src/main/java/com/fingerprint/Sealed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.fingerprint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fingerprint.model.EventResponse;
import com.fingerprint.sdk.ApiClient;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Logger;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

public class Sealed {
private static final Logger log = Logger.getLogger(ApiClient.class.getName());

public enum DecryptionAlgorithm {
AES_256_GCM
}

public static class DecryptionKey {
private final byte[] key;
private final DecryptionAlgorithm algorithm;

public DecryptionKey(byte[] key, DecryptionAlgorithm algorithm) {
this.key = key;
this.algorithm = algorithm;
}
}

private static final byte[] SEAL_HEADER = new byte[]{(byte) 0x9E, (byte) 0x85, (byte) 0xDC, (byte) 0xED};
private static final int NONCE_LENGTH = 12;
private static final int AUTH_TAG_LENGTH = 16;

public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalArgumentException {
if (!Arrays.equals(Arrays.copyOf(sealed, SEAL_HEADER.length), SEAL_HEADER)) {
throw new IllegalArgumentException("Invalid sealed data header");
}

int index = 0;

for (DecryptionKey key : keys) {
switch (key.algorithm) {
case AES_256_GCM:
try {
return decryptAes256Gcm(Arrays.copyOfRange(sealed, SEAL_HEADER.length, sealed.length), key.key);
} catch (Exception exception) {
log.warning(String.format("Failed to decrypt with key: %d error: %s", index, exception.getMessage()));
}

break;

default:
throw new IllegalArgumentException("Invalid decryption algorithm");
}

index++;
}

throw new IllegalArgumentException("Invalid decryption keys");
}

/**
* decrypts the sealed response with the provided keys.
*
* @param sealed Base64 encoded sealed data
* @param keys Decryption keys. The SDK will try to decrypt the result with each key until it succeeds.
* @return EventResponse
* @throws Exception if the sealed data is invalid or if the decryption keys are invalid
*/
public static EventResponse unsealEventResponse(byte[] sealed, DecryptionKey[] keys) throws Exception {
byte[] unsealed = unseal(sealed, keys);

ObjectMapper mapper = ObjectMapperUtil.getObjectMapper();

return mapper.readValue(unsealed, EventResponse.class);
}

private static byte[] decryptAes256Gcm(byte[] sealedData, byte[] decryptionKey) throws Exception {
byte[] nonce = Arrays.copyOfRange(sealedData, 0, NONCE_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(sealedData, NONCE_LENGTH, sealedData.length);

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec nonceSpec = new GCMParameterSpec(Byte.SIZE * AUTH_TAG_LENGTH, nonce);
SecretKeySpec keySpec = new SecretKeySpec(decryptionKey, "AES");

cipher.init(Cipher.DECRYPT_MODE, keySpec, nonceSpec);
byte[] decryptedData = cipher.doFinal(ciphertext);

// Decompressing the decrypted data
return decompress(decryptedData);
}

private static byte[] decompress(byte[] data) throws IOException {
Inflater inflater = new Inflater(true); // true for raw deflate data
InflaterInputStream inflaterInputStream = new InflaterInputStream(new ByteArrayInputStream(data), inflater);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();

int nRead;
byte[] temp = new byte[1024];
while ((nRead = inflaterInputStream.read(temp, 0, temp.length)) != -1) {
buffer.write(temp, 0, nRead);
}

return buffer.toByteArray();
}
}

83 changes: 83 additions & 0 deletions src/test/java/com/fingerprint/SealedTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.fingerprint;

import com.fingerprint.model.EventResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SealedTest {
@Test
public void unsealEventResponseTest() throws Exception {
byte[] sealedResult = Base64.getDecoder().decode("noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==");
byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=");

EventResponse expectedResponse = ObjectMapperUtil.getObjectMapper().readValue("{\"products\":{\"identification\":{\"data\":{\"visitorId\":\"2ZEDCZEfOfXjEmMuE3tq\",\"requestId\":\"1703067132750.Z5hutJ\",\"browserDetails\":{\"browserName\":\"Safari\",\"browserMajorVersion\":\"17\",\"browserFullVersion\":\"17.3\",\"os\":\"Mac OS X\",\"osVersion\":\"10.15.7\",\"device\":\"Other\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15\"},\"incognito\":false,\"ip\":\"::1\",\"ipLocation\":{\"accuracyRadius\":1000,\"latitude\":59.3241,\"longitude\":18.0517,\"postalCode\":\"100 05\",\"timezone\":\"Europe/Stockholm\",\"city\":{\"name\":\"Stockholm\"},\"country\":{\"code\":\"SE\",\"name\":\"Sweden\"},\"continent\":{\"code\":\"EU\",\"name\":\"Europe\"},\"subdivisions\":[{\"isoCode\":\"AB\",\"name\":\"Stockholm County\"}]},\"timestamp\":1703067136286,\"time\":\"2023-12-20T10:12:16Z\",\"url\":\"http://localhost:8080/\",\"tag\":{\"foo\":\"bar\"},\"confidence\":{\"score\":1},\"visitorFound\":true,\"firstSeenAt\":{\"global\":\"2023-12-15T12:13:55.103Z\",\"subscription\":\"2023-12-15T12:13:55.103Z\"},\"lastSeenAt\":{\"global\":\"2023-12-19T11:39:51.52Z\",\"subscription\":\"2023-12-19T11:39:51.52Z\"}}},\"botd\":{\"data\":{\"bot\":{\"result\":\"notDetected\"},\"meta\":{\"foo\":\"bar\"},\"url\":\"http://localhost:8080/\",\"ip\":\"::1\",\"time\":\"2023-12-20T10:12:13.894Z\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15\",\"requestId\":\"1703067132750.Z5hutJ\"}}}}", EventResponse.class);

EventResponse eventResponse = Sealed.unsealEventResponse(
sealedResult,
new Sealed.DecryptionKey[]{
new Sealed.DecryptionKey(
//Invalid key
Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="),
Sealed.DecryptionAlgorithm.AES_256_GCM
),
new Sealed.DecryptionKey(
key,
Sealed.DecryptionAlgorithm.AES_256_GCM
)
}
);

assert eventResponse.equals(expectedResponse);
}

@Test
public void unsealEventResponseWithInvalidHeaderTest() throws Exception {
byte[] sealedResult = Base64.getDecoder().decode("noXc7xXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNxlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==");
byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=");

Exception thrown = assertThrows(Exception.class, () -> Sealed.unsealEventResponse(
sealedResult,
new Sealed.DecryptionKey[]{
new Sealed.DecryptionKey(
//Invalid key
Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="),
Sealed.DecryptionAlgorithm.AES_256_GCM
),
new Sealed.DecryptionKey(
key,
Sealed.DecryptionAlgorithm.AES_256_GCM
)
}
));

assertEquals("Invalid sealed data header", thrown.getMessage());
}

@Test
public void unsealEventResponseWithInvalidKeysTest() throws Exception {
byte[] sealedResult = Base64.getDecoder().decode("noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==");

Exception thrown = assertThrows(Exception.class, () -> Sealed.unsealEventResponse(
sealedResult,
new Sealed.DecryptionKey[]{
new Sealed.DecryptionKey(
//Invalid key
Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="),
Sealed.DecryptionAlgorithm.AES_256_GCM
),
new Sealed.DecryptionKey(
Base64.getDecoder().decode("p2PA7MGy5tx56cnyJacZMr96BCFwZeHjZV2EqMvTq54="),
Sealed.DecryptionAlgorithm.AES_256_GCM
)
}
));

assertEquals("Invalid decryption keys", thrown.getMessage());
}
}
Loading

0 comments on commit 04986dc

Please sign in to comment.