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 01/11] 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}}* From eafb4373b6ea937344e749c37be477c75fd181ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 25 Jan 2024 17:16:34 +0800 Subject: [PATCH 02/11] docs: add api docs --- README.md | 5 +++++ docs/DecryptionKey.md | 10 ++++++++++ docs/Sealed.md | 12 ++++++++++++ scripts/generate.sh | 3 ++- template/README.mustache | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 docs/DecryptionKey.md create mode 100644 docs/Sealed.md diff --git a/README.md b/README.md index c0aa2c7..0c34b90 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,11 @@ Authentication schemes defined for the API: - **Location**: URL query string +## Documentation for sealed results + +- [Sealed](docs/Sealed.md) +- [DecryptionKey](docs/DecryptionKey.md) + ## Recommendation It's recommended to create an instance of `ApiClient` per thread in a multithreaded environment to avoid any potential issues. diff --git a/docs/DecryptionKey.md b/docs/DecryptionKey.md new file mode 100644 index 0000000..8964bb7 --- /dev/null +++ b/docs/DecryptionKey.md @@ -0,0 +1,10 @@ +# DecryptionKey + +## Properties + +| Name | Type | Description | Notes | +|---------------|---------------|-----------------------------------------------------------------------------------|-------| +| **Key** | **byte[]** | Key generated in dashboard that will be used to decrypt sealed result | | +| **Algorithm** | **Algorithm** | Algorithm to use for decryption. Currently only "aes-256-gcm" value is supported. | | + + diff --git a/docs/Sealed.md b/docs/Sealed.md new file mode 100644 index 0000000..2123ca0 --- /dev/null +++ b/docs/Sealed.md @@ -0,0 +1,12 @@ +# Sealed + +## **UnsealEventsResponse** +> EventResponse unsealEventResponse(sealed []byte, keys []DecryptionKey) + +Decrypts the sealed response with provided keys. +### Required Parameters + +| Name | Type | Description | Notes | +|------------|---------------------|------------------------------------------------------------------------------------------|-------| +| **sealed** | **byte[]** | Base64 encoded sealed data | | +| **keys** | **DecryptionKey[]** | Decryption keys. The SDK will try to decrypt the result with each key until it succeeds. | | diff --git a/scripts/generate.sh b/scripts/generate.sh index 54a4ffa..1e01de1 100755 --- a/scripts/generate.sh +++ b/scripts/generate.sh @@ -24,7 +24,8 @@ echo "VERSION: $VERSION" sed -i "s/artifactVersion: .*/artifactVersion: $VERSION/g" config.yaml sed -i "s/^VERSION=.*/VERSION='$VERSION'/g" ./scripts/generate.sh -rm -f pom.xml README.md build.gradle settings.gradle gradle.properties ./docs/* ./src/main/java/com/fingerprint/model/* +rm -f pom.xml README.md build.gradle settings.gradle gradle.properties ./src/main/java/com/fingerprint/model/* +find ./docs -type f ! -name "DecryptionKey.md" ! -name "Sealed.md" -exec rm {} + java -jar ./bin/generator.jar generate -c config.yaml -g java --library jersey2 -i res/fingerprint-server-api.yaml --skip-validate-spec -o . -t template diff --git a/template/README.mustache b/template/README.mustache index e35bbb0..026d01f 100644 --- a/template/README.mustache +++ b/template/README.mustache @@ -282,6 +282,11 @@ Class | Method | HTTP request | Description {{/authMethods}} +## Documentation for sealed results + +- [Sealed](docs/Sealed.md) +- [DecryptionKey](docs/DecryptionKey.md) + ## Recommendation It's recommended to create an instance of `ApiClient` per thread in a multithreaded environment to avoid any potential issues. From f80e4676a9c35c9d3cee763958483d1266b9108c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 25 Jan 2024 17:25:10 +0800 Subject: [PATCH 03/11] test: add key with invalid length --- src/test/java/com/fingerprint/SealedTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index b4182de..e836d24 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -49,6 +49,11 @@ public void unsealEventResponseWithInvalidHeaderTest() throws Exception { Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), Sealed.DecryptionAlgorithm.AES_256_GCM ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), new Sealed.DecryptionKey( key, Sealed.DecryptionAlgorithm.AES_256_GCM @@ -74,6 +79,10 @@ public void unsealEventResponseWithInvalidKeysTest() throws Exception { new Sealed.DecryptionKey( Base64.getDecoder().decode("p2PA7MGy5tx56cnyJacZMr96BCFwZeHjZV2EqMvTq54="), Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM ) } )); From a1afa5ccd187520d6403c00da87b7e88fa3418b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 26 Jan 2024 11:25:02 +0800 Subject: [PATCH 04/11] chore: add aggregate error --- src/main/java/com/fingerprint/Sealed.java | 54 ++++++++++++++----- src/test/java/com/fingerprint/SealedTest.java | 6 +-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java index 497a352..9863e71 100644 --- a/src/main/java/com/fingerprint/Sealed.java +++ b/src/main/java/com/fingerprint/Sealed.java @@ -2,7 +2,6 @@ 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; @@ -10,14 +9,13 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.logging.Logger; +import java.util.List; 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 } @@ -32,16 +30,43 @@ public DecryptionKey(byte[] key, DecryptionAlgorithm algorithm) { } } + public static class UnsealAggregateException extends Exception { + private final List unsealExceptions = new ArrayList<>(); + + public UnsealAggregateException() { + super("Failed to unseal with all decryption keys"); + } + + public void addUnsealException(UnsealException exception) { + unsealExceptions.add(exception); + } + + public List getUnsealExceptions() { + return unsealExceptions; + } + } + + public static class UnsealException extends Exception { + public final DecryptionKey decryptionKey; + + public final Exception exception; + + public UnsealException(String message, DecryptionKey decryptionKey, Exception exception) { + super(message); + this.decryptionKey = decryptionKey; + this.exception = exception; + } + } 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 { + public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalArgumentException, UnsealAggregateException { if (!Arrays.equals(Arrays.copyOf(sealed, SEAL_HEADER.length), SEAL_HEADER)) { throw new IllegalArgumentException("Invalid sealed data header"); } - int index = 0; + UnsealAggregateException aggregateException = new UnsealAggregateException(); for (DecryptionKey key : keys) { switch (key.algorithm) { @@ -49,7 +74,13 @@ public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalA 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())); + aggregateException.addUnsealException( + new UnsealException( + "Failed to decrypt", + key, + exception + ) + ); } break; @@ -57,22 +88,19 @@ public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalA default: throw new IllegalArgumentException("Invalid decryption algorithm"); } - - index++; } - throw new IllegalArgumentException("Invalid decryption keys"); + throw aggregateException; } /** * 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. + * @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 { + public static EventResponse unsealEventResponse(byte[] sealed, DecryptionKey[] keys) throws IllegalArgumentException, UnsealAggregateException, IOException { byte[] unsealed = unseal(sealed, keys); ObjectMapper mapper = ObjectMapperUtil.getObjectMapper(); diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index e836d24..67afbc0 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -6,8 +6,7 @@ import java.util.Base64; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SealedTest { @@ -68,7 +67,7 @@ public void unsealEventResponseWithInvalidHeaderTest() throws Exception { 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( + assertThrows(Sealed.UnsealAggregateException.class, () -> Sealed.unsealEventResponse( sealedResult, new Sealed.DecryptionKey[]{ new Sealed.DecryptionKey( @@ -87,6 +86,5 @@ public void unsealEventResponseWithInvalidKeysTest() throws Exception { } )); - assertEquals("Invalid decryption keys", thrown.getMessage()); } } From ba7374d494fb42dcea1331d9d874df559dccbb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 26 Jan 2024 11:35:26 +0800 Subject: [PATCH 05/11] test: add more test cases --- src/main/java/com/fingerprint/Sealed.java | 22 ++++- src/test/java/com/fingerprint/SealedTest.java | 88 ++++++++++++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java index 9863e71..2ebe500 100644 --- a/src/main/java/com/fingerprint/Sealed.java +++ b/src/main/java/com/fingerprint/Sealed.java @@ -46,6 +46,18 @@ public List getUnsealExceptions() { } } + public static class InvalidSealedDataException extends IllegalArgumentException { + public InvalidSealedDataException() { + super("Invalid sealed data"); + } + } + + public static class InvalidSealedDataHeaderException extends IllegalArgumentException { + public InvalidSealedDataHeaderException() { + super("Invalid sealed data header"); + } + } + public static class UnsealException extends Exception { public final DecryptionKey decryptionKey; @@ -63,7 +75,7 @@ public UnsealException(String message, DecryptionKey decryptionKey, Exception ex public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalArgumentException, UnsealAggregateException { if (!Arrays.equals(Arrays.copyOf(sealed, SEAL_HEADER.length), SEAL_HEADER)) { - throw new IllegalArgumentException("Invalid sealed data header"); + throw new InvalidSealedDataHeaderException(); } UnsealAggregateException aggregateException = new UnsealAggregateException(); @@ -105,7 +117,13 @@ public static EventResponse unsealEventResponse(byte[] sealed, DecryptionKey[] k ObjectMapper mapper = ObjectMapperUtil.getObjectMapper(); - return mapper.readValue(unsealed, EventResponse.class); + EventResponse value = mapper.readValue(unsealed, EventResponse.class); + + if (value.getProducts() == null) { + throw new InvalidSealedDataException(); + } + + return value; } private static byte[] decryptAes256Gcm(byte[] sealedData, byte[] decryptionKey) throws Exception { diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index 67afbc0..29b4aae 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -1,5 +1,6 @@ package com.fingerprint; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fingerprint.model.EventResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -35,12 +36,95 @@ public void unsealEventResponseTest() throws Exception { assert eventResponse.equals(expectedResponse); } + @Test + public void unsealEventResponseWithInvalidSealedResultTest() throws Exception { + // "{\"invalid\":true}" + byte[] sealedResult = Base64.getDecoder().decode("noXc7VOpBstjjcavDKSKr4HTavt4mdq8h6NC32T0hUtw9S0jXT8lPjZiWL8SyHxmrF3uTGqO+g=="); + byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53="); + + assertThrows(Sealed.InvalidSealedDataException.class, () -> Sealed.unsealEventResponse( + sealedResult, + new Sealed.DecryptionKey[]{ + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + key, + Sealed.DecryptionAlgorithm.AES_256_GCM + ) + } + )); + } + + @Test + public void unsealEventResponseWithInvalidJsonSealedResultTest() throws Exception { + byte[] sealedResult = Base64.getDecoder().decode("noXc7XdbEp5JpFNJaMxCB5leuFeW9Fs0tqvwnbU3ND2yShYn+dgeUWvdk32YrXam4yuvhmpO8gww//Qmsu2sbyvyMRuXmlKoriV9EVPYVCB2xszskg34ngrAh4sreRZV3c8d0DcXZulbMiiXrli931fEABWRHM0NtcoPuubqb+TysNSoFIYVZxpRVDR8jDiTXuQyPzvqBJD4+xeQTOOAOjPlqRTQSSBrlWjeZLNA70wWX7VRDXA1SoR+1k7bkBFK4OwRnh5rVGeGvGeHisOe/SyOL6GlQyBk3sRdSCQiI/g0ywdqLsOk4xDdCgg5vMI07APvL9FSaQrglMvD8NRmQOr5glZoV6S3DoBgaYQVvEygTZy2gfJ0z6hLY6Q8WSW0hpb3t9m4MP9WC5Vc2r0fmfqX7gjYZpwyfJxsyyk4iksminhm2T8N8DTYuZuz82jjaGNDqAPn1PZKqiEh8H9TpcgewAP8mlVrB5CUPJMHH+p7dM5zibfKM9+1MPxvZNp0PBkljBwrfGjiKlmYhn7bb5UW5TeEMtiP27KoA26PX+NV130Vi9Y/LUgMivLwaIc+jnlFyaoqg6Kg6H8G3WhT0r/pc4KP0mwyHJzfXjep8kQZGKxbMd0Sc3h4kpoWR1hdYM4QZRvKQzh7BqBPtPiVgHYoEJf9qFVxYhel9UFONz65q5bA2Y25oFKpzfsiXQqFEo/LRANnW7iUdfesGtGjjP4N6rd8ssNpYf57FmPBpWC4RwjG45MHRUSajCVLKiwUgFQbOo7/t5hgQIQOui3jmCBDjCjpjGZK8vd2nFputUTqI/MmZK7THaDPFsn8h9M1boF3VMCzDXygJFhd5lwdVErXGtQcc1lApEvdOr24QB5Io4SjfjJCfEQ7g4ulBXuqsh6I4VkcuMh5zgBIdmGm"); + byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53="); + + assertThrows(JsonMappingException.class, () -> Sealed.unsealEventResponse( + sealedResult, + new Sealed.DecryptionKey[]{ + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + key, + Sealed.DecryptionAlgorithm.AES_256_GCM + ) + } + )); + } + + @Test + public void unsealEventResponseWithNotCompressedSealedResultTest() throws Exception { + byte[] sealedResult = Base64.getDecoder().decode("noXc7dtuk0smGE+ZbaoXzrp6Rq8ySxLepejTsu7+jUXlPhV1w+WuHx9gbPhaENJnOQo8BcGmsaRhL5k2NVj+DRNzYO9cQD7wHxmXKCyTbl/dvSYOMoHziUZ2VbQ7tmaorFny26v8jROr/UBGfvPE0dLKC36IN9ZlJ3X0NZJO8SY+8bCr4mTrkVZsv/hpvZp+OjC4h7e5vxcpmnBWXzxfaO79Lq3aMRIEf9XfK7/bVIptHaEqtPKCTwl9rz1KUpUUNQSHTPM0NlqJe9bjYf5mr1uYvWHhcJoXSyRyVMxIv/quRiw3SKJzAMOTBiAvFICpWuRFa+T/xIMHK0g96w/IMQo0jdY1E067ZEvBUOBmsJnGJg1LllS3rbJVe+E2ClFNL8SzFphyvtlcfvYB+SVSD4bzI0w/YCldv5Sq42BFt5bn4n4aE5A6658DYsfSRYWqP6OpqPJx96cY34W7H1t/ZG0ulez6zF5NvWhc1HDQ1gMtXd+K/ogt1n+FyFtn8xzvtSGkmrc2jJgYNI5Pd0Z0ent73z0MKbJx9v2ta/emPEzPr3cndN5amdr6TmRkDU4bq0vyhAh87DJrAnJQLdrvYLddnrr8xTdeXxj1i1Yug6SGncPh9sbTYkdOfuamPAYOuiJVBAMcfYsYEiQndZe8mOQ4bpCr+hxAAqixhZ16pQ8CeUwa247+D2scRymLB8qJXlaERuFZtWGVAZ8VP/GS/9EXjrzpjGX9vlrIPeJP8fh2S5QPzw55cGNJ7JfAdOyManXnoEw2/QzDhSZQARVl+akFgSO0Y13YmbiL7H6HcKWGcJ2ipDKIaj2fJ7GE0Vzyt+CBEezSQR99Igd8x3p2JtvsVKp35iLPksjS1VqtSCTbuIRUlINlfQHNjeQiE/B/61jo3Mf7SmjYjqtvXt5e9RKb+CQku2qH4ZU8xN3DSg+4mLom3BgKBkm/MoyGBpMK41c96d2tRp3tp4hV0F6ac02Crg7P2lw8IUct+i2VJ8VUjcbRfTIPQs0HjNjM6/gLfLCkWOHYrlFjwusXWQCJz91Kq+hVxj7M9LtplPO4AUq6RUMNhlPGUmyOI2tcUMrjq9vMLXGlfdkH185zM4Mk+O7DRLC8683lXZFZvcBEmxr855PqLLH/9SpYKHBoGRatDRdQe3oRp6gHS0jpQ1SW/si4kvLKiUNjiBExvbQVOUV7/VFXvG1RpM9wbzSoOd40gg7ZzD/72QshUC/25DkM/Pm7RBzwtjgmnRKjT+mROeC/7VQLoz3amv09O8Mvbt+h/lX5+51Q834F7NgIGagbB20WtWcMtrmKrvCEZlaoiZrmYVSbi1RfknRK7CTPJkopw9IjO7Ut2EhKZ+jL4rwk6TlVm6EC6Kuj7KNqp6wB/UNe9eM2Eym/aiHAcja8XN4YQhSIuJD2Wxb0n3LkKnAjK1/GY65c8K6rZsVYQ0MQL1j4lMl0UZPjG/vzKyetIsVDyXc4J9ZhOEMYnt/LaxEeSt4EMJGBA9wpTmz33X4h3ij0Y3DY/rH7lrEScUknw20swTZRm5T6q1bnimj7M1OiOkebdI09MZ0nyaTWRHdB7B52C/moh89Q7qa2Fulp5h8Us1FYRkWBLt37a5rGI1IfVeP38KaPbagND+XzWpNqX4HVrAVPLQVK5EwUvGamED3ooJ0FMieTc0IH0N+IeUYG7Q8XmrRVBcw32W8pEfYLO9L71An/J0jQZCIP8DuQnUG0mOvunOuloBGvP/9LvkBlkamh68F0a5f5ny1jloyIFJhRh5dt2SBlbsXS9AKqUwARYSSsA9Ao4WJWOZMyjp8A+qIBAfW65MdhhUDKYMBgIAbMCc3uiptzElQQopE5TT5xIhwfYxa503jVzQbz1Q=="); + byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53="); + + Sealed.UnsealAggregateException exception = assertThrows(Sealed.UnsealAggregateException.class, () -> Sealed.unsealEventResponse( + sealedResult, + new Sealed.DecryptionKey[]{ + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + key, + Sealed.DecryptionAlgorithm.AES_256_GCM + ) + } + )); + + Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); + + assertEquals(unsealException.exception.getMessage(), "invalid distance too far back"); + } + @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( + assertThrows(Sealed.InvalidSealedDataHeaderException.class, () -> Sealed.unsealEventResponse( sealedResult, new Sealed.DecryptionKey[]{ new Sealed.DecryptionKey( @@ -59,8 +143,6 @@ public void unsealEventResponseWithInvalidHeaderTest() throws Exception { ) } )); - - assertEquals("Invalid sealed data header", thrown.getMessage()); } @Test From 7938cc64a608d171df280172231e9f662847e27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 26 Jan 2024 13:07:54 +0800 Subject: [PATCH 06/11] chore: fix typo --- docs/DecryptionKey.md | 8 ++++---- docs/Sealed.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/DecryptionKey.md b/docs/DecryptionKey.md index 8964bb7..8c63d8e 100644 --- a/docs/DecryptionKey.md +++ b/docs/DecryptionKey.md @@ -2,9 +2,9 @@ ## Properties -| Name | Type | Description | Notes | -|---------------|---------------|-----------------------------------------------------------------------------------|-------| -| **Key** | **byte[]** | Key generated in dashboard that will be used to decrypt sealed result | | -| **Algorithm** | **Algorithm** | Algorithm to use for decryption. Currently only "aes-256-gcm" value is supported. | | +| Name | Type | Description | Notes | +|---------------|-------------------------|-----------------------------------------------------------------------------------|-------| +| **Key** | **byte[]** | Key generated in dashboard that will be used to decrypt sealed result | | +| **Algorithm** | **DecryptionAlgorithm** | Algorithm to use for decryption. Currently only "AES_256_GCM" value is supported. | | diff --git a/docs/Sealed.md b/docs/Sealed.md index 2123ca0..e7dcdb2 100644 --- a/docs/Sealed.md +++ b/docs/Sealed.md @@ -1,7 +1,7 @@ # Sealed ## **UnsealEventsResponse** -> EventResponse unsealEventResponse(sealed []byte, keys []DecryptionKey) +> EventResponse unsealEventResponse(sealed []byte, keys DecryptionKey[]) Decrypts the sealed response with provided keys. ### Required Parameters From f0d6cf7941986c33ac3a0a768dc51c52cf7a0868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 29 Jan 2024 12:20:07 +0800 Subject: [PATCH 07/11] chore: add empty data test --- src/test/java/com/fingerprint/SealedTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index 29b4aae..a45d13f 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -145,6 +145,32 @@ public void unsealEventResponseWithInvalidHeaderTest() throws Exception { )); } + @Test + public void unsealEventResponseWithEmptyDataTest() throws Exception { + byte[] sealedResult = new byte[0]; + byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53="); + + assertThrows(Sealed.InvalidSealedDataHeaderException.class, () -> Sealed.unsealEventResponse( + sealedResult, + new Sealed.DecryptionKey[]{ + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + key, + Sealed.DecryptionAlgorithm.AES_256_GCM + ) + } + )); + } + @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=="); From 6ba59d2c9418f76f81381434ea590ca9983b7000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 30 Jan 2024 11:04:03 +0800 Subject: [PATCH 08/11] test: add empty nonce test --- src/test/java/com/fingerprint/SealedTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index a45d13f..9d3029d 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -193,6 +193,35 @@ public void unsealEventResponseWithInvalidKeysTest() throws Exception { ) } )); + } + + @Test + public void unsealEventResponseWithInvalidNonceTest() throws Exception { + byte[] sealedResult = new byte[]{(byte) 0x9E, (byte) 0x85, (byte) 0xDC, (byte) 0xED, (byte) 0xAA, (byte) 0xBB, (byte) 0xCC}; + byte[] key = Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53="); + + Sealed.UnsealAggregateException exception = assertThrows(Sealed.UnsealAggregateException.class, () -> Sealed.unsealEventResponse( + sealedResult, + new Sealed.DecryptionKey[]{ + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq54="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + //Invalid key + Base64.getDecoder().decode("aW52YWxpZA=="), + Sealed.DecryptionAlgorithm.AES_256_GCM + ), + new Sealed.DecryptionKey( + key, + Sealed.DecryptionAlgorithm.AES_256_GCM + ) + } + )); + + Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); + assertEquals(unsealException.exception.getMessage(), "12 > 3"); } } From 7f6af85fff7895f4122f372d076d9f3916ac4f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 30 Jan 2024 11:13:55 +0800 Subject: [PATCH 09/11] chore: implement PR fixes --- src/main/java/com/fingerprint/Sealed.java | 32 ++++++++++++++----- src/test/java/com/fingerprint/SealedTest.java | 4 +-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java index 2ebe500..33b349a 100644 --- a/src/main/java/com/fingerprint/Sealed.java +++ b/src/main/java/com/fingerprint/Sealed.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -28,6 +29,14 @@ public DecryptionKey(byte[] key, DecryptionAlgorithm algorithm) { this.key = key; this.algorithm = algorithm; } + + @Override + public String toString() { + return "DecryptionKey{" + + "key=" + Base64.getEncoder().encodeToString(key) + + ", algorithm=" + algorithm + + '}'; + } } public static class UnsealAggregateException extends Exception { @@ -59,16 +68,23 @@ public InvalidSealedDataHeaderException() { } public static class UnsealException extends Exception { - public final DecryptionKey decryptionKey; + public final String decryptionKeyDescription; - public final Exception exception; + public UnsealException(String message, Throwable cause, DecryptionKey decryptionKey) { + super(message, cause); + this.decryptionKeyDescription = decryptionKey.toString(); + } - public UnsealException(String message, DecryptionKey decryptionKey, Exception exception) { - super(message); - this.decryptionKey = decryptionKey; - this.exception = exception; + @Override + public String toString() { + return "UnsealException{" + + "decryptionKey=" + decryptionKeyDescription + + ", message=" + getMessage() + + ", cause=" + getCause() + + '}'; } } + 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; @@ -89,8 +105,8 @@ public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalA aggregateException.addUnsealException( new UnsealException( "Failed to decrypt", - key, - exception + exception, + key ) ); } diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index 9d3029d..6354c8a 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -116,7 +116,7 @@ public void unsealEventResponseWithNotCompressedSealedResultTest() throws Except Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); - assertEquals(unsealException.exception.getMessage(), "invalid distance too far back"); + assertEquals(unsealException.getCause().getMessage(), "invalid distance too far back"); } @Test @@ -222,6 +222,6 @@ public void unsealEventResponseWithInvalidNonceTest() throws Exception { Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); - assertEquals(unsealException.exception.getMessage(), "12 > 3"); + assertEquals(unsealException.getCause().getMessage(), "12 > 3"); } } From bf784fe19db5d5fc4845366fd8af2dedac495109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 31 Jan 2024 14:39:59 +0800 Subject: [PATCH 10/11] refactor: use addSuppressed --- src/main/java/com/fingerprint/Sealed.java | 14 +------------- src/test/java/com/fingerprint/SealedTest.java | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java index 33b349a..f8b260b 100644 --- a/src/main/java/com/fingerprint/Sealed.java +++ b/src/main/java/com/fingerprint/Sealed.java @@ -9,10 +9,8 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.List; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -40,19 +38,9 @@ public String toString() { } public static class UnsealAggregateException extends Exception { - private final List unsealExceptions = new ArrayList<>(); - public UnsealAggregateException() { super("Failed to unseal with all decryption keys"); } - - public void addUnsealException(UnsealException exception) { - unsealExceptions.add(exception); - } - - public List getUnsealExceptions() { - return unsealExceptions; - } } public static class InvalidSealedDataException extends IllegalArgumentException { @@ -102,7 +90,7 @@ public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalA try { return decryptAes256Gcm(Arrays.copyOfRange(sealed, SEAL_HEADER.length, sealed.length), key.key); } catch (Exception exception) { - aggregateException.addUnsealException( + aggregateException.addSuppressed( new UnsealException( "Failed to decrypt", exception, diff --git a/src/test/java/com/fingerprint/SealedTest.java b/src/test/java/com/fingerprint/SealedTest.java index 6354c8a..af1fa25 100644 --- a/src/test/java/com/fingerprint/SealedTest.java +++ b/src/test/java/com/fingerprint/SealedTest.java @@ -114,7 +114,7 @@ public void unsealEventResponseWithNotCompressedSealedResultTest() throws Except } )); - Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); + Sealed.UnsealException unsealException = (Sealed.UnsealException) exception.getSuppressed()[2]; assertEquals(unsealException.getCause().getMessage(), "invalid distance too far back"); } @@ -220,7 +220,7 @@ public void unsealEventResponseWithInvalidNonceTest() throws Exception { } )); - Sealed.UnsealException unsealException = exception.getUnsealExceptions().get(2); + Sealed.UnsealException unsealException = (Sealed.UnsealException) exception.getSuppressed()[2]; assertEquals(unsealException.getCause().getMessage(), "12 > 3"); } From fc529cdcf2fe8e4b67b084e48df61e1563b54038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 5 Feb 2024 20:23:14 +0800 Subject: [PATCH 11/11] docs: add throws doc --- src/main/java/com/fingerprint/Sealed.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/fingerprint/Sealed.java b/src/main/java/com/fingerprint/Sealed.java index f8b260b..8c97508 100644 --- a/src/main/java/com/fingerprint/Sealed.java +++ b/src/main/java/com/fingerprint/Sealed.java @@ -115,6 +115,10 @@ public static byte[] unseal(byte[] sealed, DecryptionKey[] keys) throws IllegalA * @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 IllegalArgumentException if invalid decryption algorithm is provided in any of the keys + * @throws UnsealAggregateException if the sealed data cannot be decrypted with any of the keys. The exception contains the list of exceptions thrown by each key. + * @throws IOException if the sealed data un-compression fails */ public static EventResponse unsealEventResponse(byte[] sealed, DecryptionKey[] keys) throws IllegalArgumentException, UnsealAggregateException, IOException { byte[] unsealed = unseal(sealed, keys);