-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add method for decoding sealed results
- Loading branch information
1 parent
2334468
commit 04986dc
Showing
8 changed files
with
343 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
38
src/examples/java/com/fingerprint/example/FunctionalTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/examples/java/com/fingerprint/example/SealedResults.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
Oops, something went wrong.