From 04986dc77ef730de9163415065535061ed5dbc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 24 Jan 2024 17:15:52 +0800 Subject: [PATCH] feat: add method for decoding sealed results --- README.md | 37 ++++++ .../java/com/fingerprint/example/EnvUtil.java | 27 +++++ .../fingerprint/example/FunctionalTests.java | 38 +----- .../fingerprint/example/SealedResults.java | 28 +++++ .../com/fingerprint/ObjectMapperUtil.java | 14 +++ src/main/java/com/fingerprint/Sealed.java | 112 ++++++++++++++++++ src/test/java/com/fingerprint/SealedTest.java | 83 +++++++++++++ template/README.mustache | 37 ++++++ 8 files changed, 343 insertions(+), 33 deletions(-) create mode 100644 src/examples/java/com/fingerprint/example/EnvUtil.java create mode 100644 src/examples/java/com/fingerprint/example/SealedResults.java create mode 100644 src/main/java/com/fingerprint/ObjectMapperUtil.java create mode 100644 src/main/java/com/fingerprint/Sealed.java create mode 100644 src/test/java/com/fingerprint/SealedTest.java diff --git a/README.md b/README.md index 801d88a..c0aa2c7 100644 --- a/README.md +++ b/README.md @@ -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* diff --git a/src/examples/java/com/fingerprint/example/EnvUtil.java b/src/examples/java/com/fingerprint/example/EnvUtil.java new file mode 100644 index 0000000..495e35e --- /dev/null +++ b/src/examples/java/com/fingerprint/example/EnvUtil.java @@ -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; + } +} diff --git a/src/examples/java/com/fingerprint/example/FunctionalTests.java b/src/examples/java/com/fingerprint/example/FunctionalTests.java index 7080e83..bd18897 100644 --- a/src/examples/java/com/fingerprint/example/FunctionalTests.java +++ b/src/examples/java/com/fingerprint/example/FunctionalTests.java @@ -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"); diff --git a/src/examples/java/com/fingerprint/example/SealedResults.java b/src/examples/java/com/fingerprint/example/SealedResults.java new file mode 100644 index 0000000..0e686d1 --- /dev/null +++ b/src/examples/java/com/fingerprint/example/SealedResults.java @@ -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); + } +} diff --git a/src/main/java/com/fingerprint/ObjectMapperUtil.java b/src/main/java/com/fingerprint/ObjectMapperUtil.java new file mode 100644 index 0000000..81f8a9f --- /dev/null +++ b/src/main/java/com/fingerprint/ObjectMapperUtil.java @@ -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; + } +} diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java new file mode 100644 index 0000000..497a352 --- /dev/null +++ b/src/main/java/com/fingerprint/Sealed.java @@ -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(); + } +} + diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java new file mode 100644 index 0000000..b4182de --- /dev/null +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -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()); + } +} diff --git a/template/README.mustache b/template/README.mustache index fa5807e..e35bbb0 100644 --- a/template/README.mustache +++ b/template/README.mustache @@ -203,6 +203,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 *{{basePath}}*