diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 809a0f4f..d99614d9 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -1,22 +1,22 @@ package io.opentdf.platform; +import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.nimbusds.jose.JOSEException; -import io.opentdf.platform.sdk.*; -import io.opentdf.platform.sdk.TDF; +import io.opentdf.platform.sdk.AssertionConfig; +import io.opentdf.platform.sdk.AutoConfigureException; +import io.opentdf.platform.sdk.Config; import io.opentdf.platform.sdk.Config.AssertionVerificationKeys; - -import com.google.gson.Gson; +import io.opentdf.platform.sdk.NanoTDF; +import io.opentdf.platform.sdk.SDK; +import io.opentdf.platform.sdk.SDKBuilder; +import io.opentdf.platform.sdk.TDF; +import nl.altindag.ssl.SSLFactory; import org.apache.commons.codec.DecoderException; -import org.bouncycastle.crypto.RuntimeCryptoException; - import picocli.CommandLine; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.Option; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -30,14 +30,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; +import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; -import java.security.KeyFactory; -import java.security.PrivateKey; import java.text.ParseException; import java.util.ArrayList; import java.util.Base64; @@ -47,11 +44,6 @@ import java.util.concurrent.ExecutionException; import java.util.function.Consumer; -import nl.altindag.ssl.SSLFactory; -import nl.altindag.ssl.util.TrustManagerUtils; - -import javax.net.ssl.TrustManager; - @CommandLine.Command( name = "tdf", subcommands = {HelpCommand.class}, @@ -234,12 +226,11 @@ private SDK buildSDK() { @CommandLine.Command(name = "decrypt") void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, + @Option(names = { "--with-assertion-verification-disabled" }, defaultValue = "false") boolean disableAssertionVerification, @Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional assertionVerification) - throws IOException, - InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, - BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC, - JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { + throws IOException, TDF.FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { var sdk = buildSDK(); + var opts = new ArrayList>(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { try (var stdout = new BufferedOutputStream(System.out)) { if (assertionVerification.isPresent()) { @@ -269,14 +260,16 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), e); } } - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig); - reader.readPayload(stdout); - } else { - var reader = new TDF().loadTDF(in, sdk.getServices().kas()); - reader.readPayload(stdout); + opts.add(Config.withAssertionVerificationKeys(assertionVerificationKeys)); } + + if (disableAssertionVerification) { + opts.add(Config.withDisableAssertionVerification(true)); + } + + var readerConfig = Config.newTDFReaderConfig(opts.toArray(new Consumer[0])); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig); + reader.readPayload(stdout); } } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java index cb0be95e..253c6207 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -52,11 +52,15 @@ public class Manifest { private static final String kAssertionSignature = "assertionSig"; private static final Gson gson = new GsonBuilder() - .registerTypeAdapter(Manifest.class, new ManifestDeserializer()) - .create(); + .registerTypeAdapter(AssertionConfig.Statement.class, new AssertionValueAdapter()) + .create(); @SerializedName(value = "schemaVersion") String tdfVersion; + public static String toJson(Manifest manifest) { + return gson.toJson(manifest); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -318,7 +322,6 @@ static public class Assertion { public String appliesToState; public AssertionConfig.Statement statement; public Binding binding; - static public class HashValues { private final String assertionHash; private final String signature; @@ -467,28 +470,43 @@ private JWSVerifier createVerifier(AssertionConfig.AssertionKey assertionKey) th } } - public EncryptionInformation encryptionInformation; - public Payload payload; - public List assertions = new ArrayList<>(); - - public static class ManifestDeserializer implements JsonDeserializer { + public static class AssertionValueAdapter implements JsonDeserializer { @Override - public Manifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - // Let Gson handle the default deserialization of the object first - Manifest manifest = new Gson().fromJson(json, typeOfT); - // Now check if the `assertions` field is null and replace it with an empty list if necessary - if (manifest.assertions == null) { - manifest.assertions = new ArrayList<>(); // Replace null with empty list + public AssertionConfig.Statement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!json.isJsonObject()) { + throw new IllegalArgumentException(String.format("%s is not a JSON object", AssertionConfig.Statement.class.getName())); + } + var obj = json.getAsJsonObject(); + var statement = new AssertionConfig.Statement(); + if (obj.has("format")) { + statement.format = obj.get("format").getAsString(); } - return manifest; + if (obj.has("schema")) { + statement.schema = obj.get("schema").getAsString(); + } + if (obj.has("value")) { + var value = obj.get("value"); + if (value.isJsonPrimitive()) { + // it's already a primitive (hopefully string) so we don't need its escaped value here + statement.value = value.getAsString(); + } else { + statement.value = value.toString(); + } + } + return statement; } } + public EncryptionInformation encryptionInformation; + public Payload payload; + public List assertions = new ArrayList<>(); protected static Manifest readManifest(Reader reader) { Manifest result = gson.fromJson(reader, Manifest.class); - if (result == null) { - throw new IllegalArgumentException("Manifest is null"); - } else if (result.payload == null) { + if (result.assertions == null) { + result.assertions = new ArrayList<>(); + } + + if (result.payload == null) { throw new IllegalArgumentException("Manifest with null payload"); } else if (result.encryptionInformation == null) { throw new IllegalArgumentException("Manifest with null encryptionInformation"); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 784bbcd0..73e7fbe3 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -7,13 +7,11 @@ import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.attributes.AttributesServiceGrpc.AttributesServiceFutureStub; import io.opentdf.platform.sdk.Config.TDFConfig; -import io.opentdf.platform.sdk.Manifest.ManifestDeserializer; import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; import io.opentdf.platform.sdk.Config.KASInfo; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; -import org.erdtman.jcs.JsonCanonicalizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,9 +77,7 @@ public TDF() { private static final SecureRandom sRandom = new SecureRandom(); - private static final Gson gson = new GsonBuilder() - .registerTypeAdapter(Manifest.class, new ManifestDeserializer()) - .create(); + private static final Gson gson = new GsonBuilder().create(); public class SplitKeyException extends IOException { public SplitKeyException(String errorMessage) { @@ -561,7 +557,7 @@ public TDFObject createTDF(InputStream payload, } tdfObject.manifest.assertions = signedAssertions; - String manifestAsStr = gson.toJson(tdfObject.manifest); + String manifestAsStr = Manifest.toJson(tdfObject.manifest); tdfWriter.appendManifest(manifestAsStr); tdfObject.size = tdfWriter.finish(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java index 7a3a9c77..98ade257 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java @@ -1,17 +1,15 @@ package io.opentdf.platform.sdk; -import org.junit.jupiter.api.Test; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import io.opentdf.platform.sdk.Manifest.ManifestDeserializer; +import org.junit.jupiter.api.Test; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; import java.util.List; import java.util.Map; -import static org.junit.Assert.assertNotNull; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; public class ManifestTest { @@ -64,15 +62,11 @@ void testManifestMarshalAndUnMarshal() { " }\n" + "}"; - GsonBuilder gsonBuilder = new GsonBuilder(); - Gson gson = gsonBuilder.setPrettyPrinting() - .registerTypeAdapter(Manifest.class, new ManifestDeserializer()) - .create(); - Manifest manifest = gson.fromJson(kManifestJsonFromTDF, Manifest.class); + Manifest manifest = Manifest.readManifest(new StringReader(kManifestJsonFromTDF)); // Test payload assertEquals(manifest.payload.url, "0.payload"); - assertEquals(manifest.payload.isEncrypted, true); + assertThat(manifest.payload.isEncrypted).isTrue(); // Test encryptionInformation assertEquals(manifest.encryptionInformation.keyAccessType, "split"); @@ -90,8 +84,8 @@ void testManifestMarshalAndUnMarshal() { assertEquals(manifest.encryptionInformation.integrityInformation.segmentHashAlg, "GMAC"); assertEquals(manifest.encryptionInformation.integrityInformation.segments.get(0).segmentSize, 1048576); - var serialized = gson.toJson(manifest); - var deserializedAgain = gson.fromJson(serialized, Manifest.class); + var serialized = Manifest.toJson(manifest); + var deserializedAgain = Manifest.readManifest(new StringReader(serialized)); assertEquals(manifest, deserializedAgain, "something changed when we deserialized -> serialized -> deserialized"); } @@ -146,18 +140,43 @@ void testAssertionNull() { " \"assertions\": null\n"+ "}"; - GsonBuilder gsonBuilder = new GsonBuilder(); - Gson gson = gsonBuilder.setPrettyPrinting() - .registerTypeAdapter(Manifest.class, new ManifestDeserializer()) - .create(); - Manifest manifest = gson.fromJson(kManifestJsonFromTDF, Manifest.class); + Manifest manifest = Manifest.readManifest(new StringReader(kManifestJsonFromTDF)); // Test payload for sanity check assertEquals(manifest.payload.url, "0.payload"); - assertEquals(manifest.payload.isEncrypted, true); + assertThat(manifest.payload.isEncrypted).isTrue(); // Test assertion deserialization - assertNotNull(manifest.assertions); + assertThat(manifest.assertions).isNotNull(); assertEquals(manifest.assertions.size(), 0); + } + + @Test + void testReadingManifestWithObjectStatementValue() throws IOException { + final Manifest manifest; + try (var mStream = getClass().getResourceAsStream("/io.opentdf.platform.sdk.TestData/manifest-with-object-statement-value.json")) { + assert mStream != null; + manifest = Manifest.readManifest(new InputStreamReader(mStream)) ; + } + + assertThat(manifest.assertions).hasSize(2); + var statementValStr = manifest.assertions.get(0).statement.value; + var statementVal = new Gson().fromJson(statementValStr, Map.class); + assertThat(statementVal).isEqualTo( + Map.of("ocl", + Map.of("pol", "2ccf11cb-6c9a-4e49-9746-a7f0a295945d", + "cls", "SECRET", + "catl", List.of( + Map.of( + "type", "P", + "name", "Releasable To", + "vals", List.of("usa") + ) + ), + "dcr", "2024-12-17T13:00:52Z" + ), + "context", Map.of("@base", "urn:nato:stanag:5636:A:1:elements:json") + ) + ); } } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java index f9819193..a66e0f84 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java @@ -1,8 +1,6 @@ package io.opentdf.platform.sdk; -import com.google.gson.GsonBuilder; - -import io.opentdf.platform.sdk.Manifest.ManifestDeserializer; +import com.google.gson.Gson; import org.apache.commons.compress.archivers.zip.Zip64Mode; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; @@ -22,9 +20,6 @@ import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; public class ZipReaderTest { @@ -47,9 +42,7 @@ protected static void testReadingZipChannel(SeekableByteChannel fileChannel, boo if (entry.getName().endsWith(".json")) { entry.getData().transferTo(stream); var data = stream.toString(StandardCharsets.UTF_8); - var gson = new GsonBuilder() - .registerTypeAdapter(Manifest.class, new ManifestDeserializer()) - .create(); + var gson = new Gson(); var map = gson.fromJson(data, Map.class); if (test) { diff --git a/sdk/src/test/resources/io.opentdf.platform.sdk.TestData/manifest-with-object-statement-value.json b/sdk/src/test/resources/io.opentdf.platform.sdk.TestData/manifest-with-object-statement-value.json new file mode 100644 index 00000000..f0c9e319 --- /dev/null +++ b/sdk/src/test/resources/io.opentdf.platform.sdk.TestData/manifest-with-object-statement-value.json @@ -0,0 +1,129 @@ +{ + "payload": { + "type": "reference", + "url": "0.payload", + "protocol": "zip", + "isEncrypted": true, + "mimeType": "text/plain", + "tdf_spec_version": null + }, + "encryptionInformation": { + "type": "split", + "keyAccess": [ + { + "type": "wrapped", + "url": "https://virtru-eng-kas.apps.dsp.shp.virtru.us/kas", + "protocol": "kas", + "wrappedKey": "z88k9ZYiu6aMz/3BYvS7+K89EYFN/r2/uZ2XdoIOLdk6fa/5FkNgKIy1KcHL4e+cLQEJagOoqB6X7drGRCxpBNh2GLcKnSxjkvCipVNVY03o+QMhXKBf1xx5ZtFokRHxkflAYlHrTtF5cN25RbSLTUso9zB6Jex3p9mnjYeN3b1BjarkKn6//hKjQFYAGWFygFMaM+iNXBO6dAcTg204RI4h+cAk404lYvN2FGW4wZOu0igi8Es80vcYm3WQT/h3MrNqxN5EMuvrF8tmviSwUH/EpHw4s+5YgAuZIheBPujURhnbq+YKJuAl30ro/POUO60TrTcTmkpaLswQBCBeLg==", + "sid": null, + "kid": "r1", + "policyBinding": { + "alg": "HS256", + "hash": "MmZjN2RhNjM1ZTEyNWM4NjVhYTQwYWQzNjcyYWRiZGViYTdlNDA1ZTVkOTAyZGViNmQwM2Q1ZWIxNDM3NTE5Ng==" + }, + "encryptedMetadata": null + }, + { + "type": "wrapped", + "url": "https://platform-dsp-pep-qa.apps.dsp.shp.virtru.us/kas", + "protocol": "kas", + "wrappedKey": "FZ5lGT9qG5fQJ16v+gCM9huPavBhh/ooPfFw36wJkW+KwPlnOkBvJuTVq9S7iUzUKgfGSd80TxfvVCS0qds4gFS+oAKxKg/CSVILbyyPYaNgpR8/dFFKAFb2w/9xhocE1uvfrQkyXrDAnyAgNRNm1ecpGkYuOrjPCb++v+6+DjQxscQcBuMzG8J5lRMB/VdME1VtT/VCZ4JGZcpXJfDKnZjYY/6fUFGLK7sOTYLo5nmVvapDLsmaQXFyBfo30oDbsnPTofk1EMT5DMrv5H6UlK7YiGda9OTsQK6t0yrK84oBoTRJmH4BEMs7oDeFDzNZGHp8bjq/IWF/zzu/haRMZw==", + "sid": null, + "kid": "r1", + "policyBinding": { + "alg": "HS256", + "hash": "MmZjN2RhNjM1ZTEyNWM4NjVhYTQwYWQzNjcyYWRiZGViYTdlNDA1ZTVkOTAyZGViNmQwM2Q1ZWIxNDM3NTE5Ng==" + }, + "encryptedMetadata": null + } + ], + "method": { + "algorithm": "AES-256-GCM", + "isStreamable": true, + "iv": null + }, + "integrityInformation": { + "rootSignature": { + "alg": "HS256", + "sig": "MjM2NjAzMjZiNWJhODdhYTc4OGJmODJlZWM5YmExYmRmN2Q4YjUzYzVkOWI1NmI1MmM1MDBmNTY2OWNkZTUxYw==" + }, + "segmentSizeDefault": 104857600, + "segmentHashAlg": "GMAC", + "segments": [ + { + "hash": "ZDZlYjg0ZjhjYWMyMmU5YmNiZTdhYmJhMDcwMTk4NTg=", + "segmentSize": 24, + "encryptedSegmentSize": 52 + } + ], + "encryptedSegmentSizeDefault": 104857628 + }, + "policy": "eyJ1dWlkIjoiMzI5NDZhNTctYjJiZS00M2JmLWFhY2YtZjVjMjZlYzYxY2E0IiwiYm9keSI6eyJkYXRhQXR0cmlidXRlcyI6W3siZGVzY3JpcHRpb24iOm51bGwsInR5cGUiOm51bGwsImF0dHJpYnV0ZSI6Imh0dHBzOi8vZGVtby5jb20vYXR0ci9yZWx0by92YWx1ZS91c2EiLCJkaXNwbGF5TmFtZSI6bnVsbCwia2FzVVJMIjoiaHR0cHM6Ly92aXJ0cnUtZW5nLWthcy5hcHBzLmRzcC5zaHAudmlydHJ1LnVzL2thcyxodHRwczovL3BsYXRmb3JtLWRzcC1wZXAtcWEuYXBwcy5kc3Auc2hwLnZpcnRydS51cy9rYXMiLCJwdWJLZXkiOm51bGwsImlzRGVmYXVsdCI6bnVsbH0seyJkZXNjcmlwdGlvbiI6bnVsbCwidHlwZSI6bnVsbCwiYXR0cmlidXRlIjoiaHR0cHM6Ly9kZW1vLmNvbS9hdHRyL2NsYXNzaWZpY2F0aW9uL3ZhbHVlL3NlY3JldCIsImRpc3BsYXlOYW1lIjpudWxsLCJrYXNVUkwiOiJodHRwczovL3ZpcnRydS1lbmcta2FzLmFwcHMuZHNwLnNocC52aXJ0cnUudXMva2FzLGh0dHBzOi8vcGxhdGZvcm0tZHNwLXBlcC1xYS5hcHBzLmRzcC5zaHAudmlydHJ1LnVzL2thcyIsInB1YktleSI6bnVsbCwiaXNEZWZhdWx0IjpudWxsfV0sImRpc3NlbSI6W119fQ==" + }, + "assertions": [ + { + "id": "bacbe31eab384df39d35a5fbe83778de", + "type": "handling", + "scope": "tdo", + "appliesToState": null, + "statement": { + "format": "json-structured", + "value": { + "ocl": { + "pol": "2ccf11cb-6c9a-4e49-9746-a7f0a295945d", + "cls": "SECRET", + "catl": [ + { + "type": "P", + "name": "Releasable To", + "vals": [ + "usa" + ] + } + ], + "dcr": "2024-12-17T13:00:52Z" + }, + "context": { + "@base": "urn:nato:stanag:5636:A:1:elements:json" + } + } + }, + "binding": { + "method": "jws", + "signature": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJDb25maWRlbnRpYWxpdHlJbmZvcm1hdGlvbiI6InsgXCJvY2xcIjogeyBcInBvbFwiOiBcIjJjY2YxMWNiLTZjOWEtNGU0OS05NzQ2LWE3ZjBhMjk1OTQ1ZFwiLCBcImNsc1wiOiBcIlNFQ1JFVFwiLCBcImNhdGxcIjogWyB7IFwidHlwZVwiOiBcIlBcIiwgXCJuYW1lXCI6IFwiUmVsZWFzYWJsZSBUb1wiLCBcInZhbHNcIjogWyBcInVzYVwiIF0gfSBdLCBcImRjclwiOiBcIjIwMjQtMTItMTdUMTM6MDA6NTJaXCIgfSwgXCJjb250ZXh0XCI6IHsgXCJAYmFzZVwiOiBcInVybjpuYXRvOnN0YW5hZzo1NjM2OkE6MTplbGVtZW50czpqc29uXCIgfSB9In0.LlOzRLKKXMAqXDNsx9Ha5915CGcAkNLuBfI7jJmx6CnfQrLXhlRHWW3_aLv5DPsKQC6vh9gDQBH19o7q7EcukvK4IabA4l0oP8ePgHORaajyj7ONjoeudv_zQ9XN7xU447S3QznzOoasuWAFoN4682Fhf99Kjl6rhDCzmZhTwQw9drP7s41nNA5SwgEhoZj-X9KkNW5GbWjA95eb8uVRRWk8dOnVje6j8mlJuOtKdhMxQ8N5n0vBYYhiss9c4XervBjWAxwAMdbRaQN0iPZtMzIkxKLYxBZDvTnYSAqzpvfGPzkSI-Ze_hUZs2hp-ADNnYUJBf_LzFmKyqHjPSFQ7A" + } + }, + { + "id": "ab43266781e64b51a4c52ffc44d6152c", + "type": "handling", + "scope": "payload", + "appliesToState": null, + "statement": { + "format": "json-structured", + "value": { + "ocl": { + "pol": "2ccf11cb-6c9a-4e49-9746-a7f0a295945d", + "cls": "SECRET", + "catl": [ + { + "type": "P", + "name": "Releasable To", + "vals": [ + "usa" + ] + } + ], + "dcr": "2024-12-17T13:00:52Z" + }, + "context": { + "@base": "urn:nato:stanag:5636:A:1:elements:json" + } + } + }, + "binding": { + "method": "jws", + "signature": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJDb25maWRlbnRpYWxpdHlJbmZvcm1hdGlvbiI6InsgXCJvY2xcIjogeyBcInBvbFwiOiBcIjJjY2YxMWNiLTZjOWEtNGU0OS05NzQ2LWE3ZjBhMjk1OTQ1ZFwiLCBcImNsc1wiOiBcIlNFQ1JFVFwiLCBcImNhdGxcIjogWyB7IFwidHlwZVwiOiBcIlBcIiwgXCJuYW1lXCI6IFwiUmVsZWFzYWJsZSBUb1wiLCBcInZhbHNcIjogWyBcInVzYVwiIF0gfSBdLCBcImRjclwiOiBcIjIwMjQtMTItMTdUMTM6MDA6NTJaXCIgfSwgXCJjb250ZXh0XCI6IHsgXCJAYmFzZVwiOiBcInVybjpuYXRvOnN0YW5hZzo1NjM2OkE6MTplbGVtZW50czpqc29uXCIgfSB9IiwiUGF5bG9hZFNoYTI1NiI6IjlRMHVNZW9WTnVEVlVBVmxaelpMSjVMSzFTSkI3bjVHeUU1Rnp4bWh2WFU9In0.PZ2VF22D-MDIhgHvTKQmABE5AFpYr89q5iq9QIeIcx-lYN9NF0mptWWdVtrOog16NXJF5WZlwGXkMpiSJ2N16lQjRkc48MobHOwHMW3IU5sTeEWsPS9jc5SLh8HziKPKnHBlPWkrEGY_QUifkXFOgHihuXnZh1mcMxZt03mP3VeFuGRoWl_ZfhT6UT0oTDtZL4bobMODUYzCLCiCPMAE1AOqepWQBxRf55tqAAYTY5pvhO5CwXSenQTIowDfy7ULPPc_Kee8g59s4Xwe7-7d7mAcf30R3cKa5JZffE3p6W1dUh4OZp2N7wprXlCZYXZ5qWW6o8ACtXrmC8MzWj7Jjg" + } + } + ] +}