From da8eb6630fa77c7b0f21fe99db61dbcad094730b Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 9 Sep 2026 08:49:49 -0400 Subject: [PATCH 1/3] fix(sdk): DSPX-4589 default absent per-segment sizes to the manifest defaults integrityInformation.segments[].segmentSize and .encryptedSegmentSize are optional in the TDF spec: manifest.schema.json marks segmentSizeDefault and encryptedSegmentSizeDefault required on integrityInformation but puts no required list on segments/items, so an absent per-segment value means "the default", not zero. Gson leaves an absent primitive at 0, so java-sdk read those segments with a zero length buffer and failed inside the integrity check. web-sdk omits a per-segment size whenever it equals the default, which is every full segment, so every web-sdk TDF larger than one default segment (1 MiB) failed to decrypt, surfacing as a confusing integrity error rather than as a manifest problem. A primitive long cannot distinguish an absent JSON key from a literal 0, so a Gson TypeAdapterFactory registered for IntegrityInformation walks the parsed segments array alongside the deserialized list and fills in the defaults only where the key is absent or JSON null. Boxing Segment.segmentSize to Long was the other option, but it breaks the public API for no added behavior. An explicit 0 in the JSON is preserved as 0. TDF.Reader.readPayload additionally rejects a segment with a non-positive encryptedSegmentSize up front -- an encrypted segment always carries at least an IV and a tag -- so a manifest that supplies neither a per-segment size nor a usable default now says so instead of failing downstream with an unrelated complaint about the payload being too small to GMAC. --- .../io/opentdf/platform/sdk/Manifest.java | 95 +++++++++++++ .../java/io/opentdf/platform/sdk/TDF.java | 13 ++ .../io/opentdf/platform/sdk/ManifestTest.java | 70 ++++++++++ .../java/io/opentdf/platform/sdk/TDFTest.java | 125 ++++++++++++++++++ 4 files changed, 303 insertions(+) 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 20fbb13e..5f241fc0 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -6,10 +6,17 @@ import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -56,6 +63,7 @@ public class Manifest { private static final Gson gson = new GsonBuilder() .registerTypeAdapter(AssertionConfig.Statement.class, new AssertionValueAdapter()) + .registerTypeAdapterFactory(new IntegrityInformationAdapterFactory()) .create(); @SerializedName(value = "schemaVersion") String tdfVersion; @@ -98,7 +106,17 @@ public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContex static public class Segment { public String hash; + /** + * The plaintext length of this segment. Optional in the JSON: when a producer leaves it + * out it means {@link IntegrityInformation#segmentSizeDefault}, which + * {@link IntegrityInformationAdapterFactory} fills in during deserialization. + */ public long segmentSize; + /** + * The on-the-wire length of this segment. Optional in the JSON the same way + * {@link #segmentSize} is, defaulting to + * {@link IntegrityInformation#encryptedSegmentSizeDefault}. + */ public long encryptedSegmentSize; @Override @@ -167,6 +185,83 @@ public int hashCode() { } } + /** + * Applies {@code segmentSizeDefault} / {@code encryptedSegmentSizeDefault} to any segment + * that left the corresponding per-segment key out of its JSON. + *

+ * The per-segment values are optional overrides: {@code manifest.schema.json} marks the two + * defaults required on {@code integrityInformation} but puts no {@code required} list on + * {@code segments/items}. web-sdk omits a per-segment size whenever it equals the default, + * which is every full segment of a payload larger than one segment. Gson leaves an absent + * key at {@code 0}, so before this the reader allocated a zero length buffer for those + * segments and failed inside the integrity check. + *

+ * This runs as a post-deserialization fixup rather than by boxing the fields to {@code Long}, + * which keeps {@link Segment#segmentSize} a primitive for callers, keeps the value/absence + * distinction out of the public API, and applies to every path that parses a manifest. A + * primitive alone cannot tell an absent key from a literal {@code 0}, so the decision is made + * against the parse tree rather than against the deserialized value. + */ + private static class IntegrityInformationAdapterFactory implements TypeAdapterFactory { + private static final String SEGMENTS = "segments"; + private static final String SEGMENT_SIZE = "segmentSize"; + private static final String ENCRYPTED_SEGMENT_SIZE = "encryptedSegmentSize"; + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (!IntegrityInformation.class.equals(type.getRawType())) { + return null; + } + final TypeAdapter delegate = gson.getDelegateAdapter(this, type); + final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); + return new TypeAdapter() { + @Override + public void write(JsonWriter out, T value) throws IOException { + delegate.write(out, value); + } + + @Override + public T read(JsonReader in) throws IOException { + JsonElement tree = elementAdapter.read(in); + T value = delegate.fromJsonTree(tree); + if (value instanceof IntegrityInformation && tree != null && tree.isJsonObject()) { + applySegmentSizeDefaults((IntegrityInformation) value, tree.getAsJsonObject()); + } + return value; + } + }; + } + + private static void applySegmentSizeDefaults(IntegrityInformation integrityInformation, JsonObject json) { + List segments = integrityInformation.segments; + JsonElement rawSegments = json.get(SEGMENTS); + if (segments == null || rawSegments == null || !rawSegments.isJsonArray()) { + return; + } + JsonArray rawSegmentArray = rawSegments.getAsJsonArray(); + int count = Math.min(segments.size(), rawSegmentArray.size()); + for (int i = 0; i < count; i++) { + Segment segment = segments.get(i); + JsonElement rawSegment = rawSegmentArray.get(i); + if (segment == null || rawSegment == null || !rawSegment.isJsonObject()) { + continue; + } + JsonObject rawSegmentObject = rawSegment.getAsJsonObject(); + if (!hasValue(rawSegmentObject, SEGMENT_SIZE)) { + segment.segmentSize = integrityInformation.segmentSizeDefault; + } + if (!hasValue(rawSegmentObject, ENCRYPTED_SEGMENT_SIZE)) { + segment.encryptedSegmentSize = integrityInformation.encryptedSegmentSizeDefault; + } + } + } + + private static boolean hasValue(JsonObject object, String memberName) { + JsonElement member = object.get(memberName); + return member != null && !member.isJsonNull(); + } + } + static public class PolicyBinding { public String alg; public String hash; 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 b54901b9..f18d1ce1 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -421,6 +421,19 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi } for (Manifest.Segment segment : manifest.encryptionInformation.integrityInformation.segments) { + if (segment.encryptedSegmentSize <= 0) { + // an encrypted segment always carries at least an IV and a tag, so this only + // happens on a manifest that supplied neither a per-segment + // encryptedSegmentSize nor a usable encryptedSegmentSizeDefault. reported + // here rather than letting a zero length buffer reach the integrity check, + // where it surfaces as an unrelated complaint about the payload being too + // small to GMAC + throw new IllegalStateException("invalid TDF: segment has an encrypted size of " + + segment.encryptedSegmentSize + + ". the manifest supplied neither a per-segment encryptedSegmentSize" + + " nor a usable encryptedSegmentSizeDefault"); + } + if (segment.encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " + Config.MAX_SEGMENT_SIZE); 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 220ca6d1..fcc720d6 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java @@ -149,6 +149,76 @@ void testAssertionNull() { assertEquals(manifest.assertions.size(), 0); } + private static final long SEGMENT_SIZE_DEFAULT = 1048576; + private static final long ENCRYPTED_SEGMENT_SIZE_DEFAULT = 1048604; + + /** A minimal but valid manifest wrapped around whatever {@code segments} array you give it. */ + private static String manifestWithSegments(String segmentsJson) { + return "{\n" + + " \"encryptionInformation\": {\n" + + " \"integrityInformation\": {\n" + + " \"encryptedSegmentSizeDefault\": " + ENCRYPTED_SEGMENT_SIZE_DEFAULT + ",\n" + + " \"rootSignature\": { \"alg\": \"HS256\", \"sig\": \"c2ln\" },\n" + + " \"segmentHashAlg\": \"GMAC\",\n" + + " \"segmentSizeDefault\": " + SEGMENT_SIZE_DEFAULT + ",\n" + + " \"segments\": [" + segmentsJson + "]\n" + + " },\n" + + " \"keyAccess\": [ { \"protocol\": \"kas\", \"type\": \"wrapped\"," + + " \"url\": \"http://localhost:65432/kas\", \"wrappedKey\": \"a2V5\" } ],\n" + + " \"method\": { \"algorithm\": \"AES-256-GCM\", \"isStreamable\": true, \"iv\": \"aXY=\" },\n" + + " \"policy\": \"cG9saWN5\",\n" + + " \"type\": \"split\"\n" + + " },\n" + + " \"payload\": { \"isEncrypted\": true, \"protocol\": \"zip\"," + + " \"type\": \"reference\", \"url\": \"0.payload\" }\n" + + "}"; + } + + /** + * web-sdk leaves {@code segmentSize} and {@code encryptedSegmentSize} out of a segment + * whenever they equal the manifest level defaults. That is legal: {@code manifest.schema.json} + * marks the two defaults required on {@code integrityInformation} but puts no + * {@code required} list on {@code segments/items}, so the per-segment values are optional + * overrides and an absent one means "the default", not zero. + */ + @Test + void testAbsentSegmentSizesFallBackToTheManifestDefaults() { + Manifest manifest = Manifest.readManifest(manifestWithSegments( + "{ \"hash\": \"aGFzaDA=\" }," + + "{ \"hash\": \"aGFzaDE=\", \"segmentSize\": 12 }," + + "{ \"hash\": \"aGFzaDI=\", \"segmentSize\": 3, \"encryptedSegmentSize\": 31 }")); + + var segments = manifest.encryptionInformation.integrityInformation.segments; + assertThat(segments).hasSize(3); + + assertThat(segments.get(0).segmentSize).isEqualTo(SEGMENT_SIZE_DEFAULT); + assertThat(segments.get(0).encryptedSegmentSize).isEqualTo(ENCRYPTED_SEGMENT_SIZE_DEFAULT); + + assertThat(segments.get(1).segmentSize).isEqualTo(12); + assertThat(segments.get(1).encryptedSegmentSize).isEqualTo(ENCRYPTED_SEGMENT_SIZE_DEFAULT); + + assertThat(segments.get(2).segmentSize).isEqualTo(3); + assertThat(segments.get(2).encryptedSegmentSize).isEqualTo(31); + + // and the values we filled in survive a round trip through the serializer + assertEquals(manifest, Manifest.readManifest(Manifest.toJson(manifest))); + } + + /** + * An explicit zero is a value rather than an absent key, so it is left alone. The reader + * rejects it with its own error; silently rewriting it to the default would hide a corrupt + * manifest. + */ + @Test + void testExplicitZeroSegmentSizeIsNotTreatedAsAbsent() { + Manifest manifest = Manifest.readManifest(manifestWithSegments( + "{ \"hash\": \"aGFzaDA=\", \"segmentSize\": 0, \"encryptedSegmentSize\": 0 }")); + + var segment = manifest.encryptionInformation.integrityInformation.segments.get(0); + assertThat(segment.segmentSize).isZero(); + assertThat(segment.encryptedSegmentSize).isZero(); + } + @Test void testReadingManifestWithObjectStatementValue() throws IOException { final Manifest manifest; diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 3fbb3eac..172294c2 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -7,6 +7,7 @@ import com.nimbusds.jose.jwk.JWK; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; @@ -34,6 +35,7 @@ import java.util.Random; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -46,6 +48,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class TDFTest { protected static KeyAccessServerRegistryServiceClient kasRegistryService; @@ -731,6 +734,128 @@ public void testCreatingTDFWithMultipleSegments() throws Exception { } + /** web-sdk's {@code DEFAULT_SEGMENT_SIZE}, the size at which its omissions start. */ + private static final int WEB_SDK_DEFAULT_SEGMENT_SIZE = 1024 * 1024; + + /** + * web-sdk drops {@code segmentSize} and {@code encryptedSegmentSize} from a segment whenever + * they equal the manifest level defaults, which is every full segment of a payload larger + * than one segment. Reproduces that encoding on a java-produced TDF rather than carrying a + * web-sdk fixture, so the test stays self-contained. Without the fallback the reader + * allocates a zero length buffer for those segments and fails inside the integrity check. + */ + @Test + public void testReadingATDFThatOmitsDefaultedSegmentSizes() throws Exception { + // two full segments and a partial one, the shape that first exposed this + var data = new byte[2 * WEB_SDK_DEFAULT_SEGMENT_SIZE + 4242]; + new Random(4589).nextBytes(data); + + var tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var original = new ByteArrayOutputStream(); + var tdfObject = tdf.createTDF(new ByteArrayInputStream(data), original, + Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(WEB_SDK_DEFAULT_SEGMENT_SIZE))); + assertThat(tdfObject.getManifest().encryptionInformation.integrityInformation.segments) + .withFailMessage("the test needs more than one segment to be meaningful") + .hasSizeGreaterThan(1); + + var rewritten = rewriteManifest(original.toByteArray(), manifest -> { + var integrityInformation = manifest.getAsJsonObject("encryptionInformation") + .getAsJsonObject("integrityInformation"); + var segmentSizeDefault = integrityInformation.get("segmentSizeDefault").getAsLong(); + var encryptedSegmentSizeDefault = integrityInformation.get("encryptedSegmentSizeDefault").getAsLong(); + + int omitted = 0; + for (var element : integrityInformation.getAsJsonArray("segments")) { + var segment = element.getAsJsonObject(); + if (segment.get("segmentSize").getAsLong() == segmentSizeDefault) { + segment.remove("segmentSize"); + omitted++; + } + if (segment.get("encryptedSegmentSize").getAsLong() == encryptedSegmentSizeDefault) { + segment.remove("encryptedSegmentSize"); + } + } + assertThat(omitted) + .withFailMessage("no segment matched the defaults, so nothing was omitted") + .isGreaterThan(0); + }); + + var unwrapped = new ByteArrayOutputStream(); + tdf.loadTDF(new SeekableInMemoryByteChannel(rewritten), platformUrl).readPayload(unwrapped); + + assertThat(unwrapped.toByteArray()) + .withFailMessage("extracted data does not match") + .containsExactly(data); + } + + /** + * An explicit zero is a corrupt manifest rather than an omitted default, and has to say so + * instead of reaching the integrity check and complaining that the payload is too small to + * GMAC. + */ + @Test + public void testZeroLengthSegmentIsRejectedWithAClearError() throws Exception { + var data = "some data to encrypt".getBytes(StandardCharsets.UTF_8); + var tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var original = new ByteArrayOutputStream(); + tdf.createTDF(new ByteArrayInputStream(data), original, + Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()))); + + var rewritten = rewriteManifest(original.toByteArray(), manifest -> manifest + .getAsJsonObject("encryptionInformation") + .getAsJsonObject("integrityInformation") + .getAsJsonArray("segments") + .get(0).getAsJsonObject() + .addProperty("encryptedSegmentSize", 0)); + + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(rewritten), platformUrl); + assertThatThrownBy(() -> reader.readPayload(new ByteArrayOutputStream())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("encrypted size of 0"); + } + + /** Rebuilds a TDF with its manifest edited in place, leaving the payload bytes untouched. */ + private static byte[] rewriteManifest(byte[] tdfBytes, Consumer edit) throws IOException { + final JsonObject manifest; + final byte[] payload; + try (var channel = new SeekableInMemoryByteChannel(tdfBytes)) { + var reader = new ZipReader(channel); + manifest = JsonParser + .parseString(readZipEntry(reader, TDFWriter.TDF_MANIFEST_FILE_NAME) + .toString(StandardCharsets.UTF_8)) + .getAsJsonObject(); + payload = readZipEntry(reader, TDFWriter.TDF_PAYLOAD_FILE_NAME).toByteArray(); + } + + edit.accept(manifest); + + var out = new ByteArrayOutputStream(); + var writer = new TDFWriter(out); + try (var payloadStream = writer.payload()) { + payloadStream.write(payload); + } + writer.appendManifest(manifest.toString()); + writer.finish(); + return out.toByteArray(); + } + + private static ByteArrayOutputStream readZipEntry(ZipReader reader, String name) throws IOException { + var entry = reader.getEntries().stream() + .filter(e -> e.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new AssertionError("no entry named " + name)); + var data = new ByteArrayOutputStream(); + entry.getData().transferTo(data); + return data; + } + /** * The unsigned 96-bit big-endian encoding of {@code value}, for asserting on * expected IVs. From c2228d2ac0827bf35b60e2195ca4106c79bc2207 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 9 Sep 2026 11:32:45 -0400 Subject: [PATCH 2/3] fix(sdk): DSPX-4589 address review feedback on segment size defaults Correct the guard added for undersized segments and the comments around it. - the old error message blamed a missing encryptedSegmentSizeDefault, but loadTDF already rejects a manifest whose two defaults disagree, so the only way to reach the guard is an explicitly bad per-segment value. say what was observed instead, and name the offending segment. - the guard's rationale is an IV plus an auth tag, but it only checked for a non-positive size. sizes 1..27 still reached the integrity check as a signature mismatch or a GMAC-too-small complaint. raise the floor to kGcmIvSize + GCM_TAG_LENGTH, keeping a positive-only floor for the unencrypted payload branch, where segments carry neither. - hoist the size checks into a pre-pass so an invalid size on a later segment no longer leaves the caller holding the earlier plaintext. - the end-to-end test only asserted that a segmentSize was omitted, but plaintext segmentSize is write-only here: the encryptedSegmentSize omission is what the reader depends on, and it was unasserted. count both, and cover the exact-multiple shape where every segment omits both. - cite the schema by its real path and verify the claim; drop the duplicated copy in the test. document that an explicit null defaults while an explicit zero does not, and that absent defaults leave zeroes. - assert the segments/JSON array size invariant rather than silently iterating the shorter of the two. --- .../io/opentdf/platform/sdk/Manifest.java | 59 ++++++----- .../java/io/opentdf/platform/sdk/TDF.java | 55 +++++++---- .../io/opentdf/platform/sdk/ManifestTest.java | 60 +++++++++--- .../java/io/opentdf/platform/sdk/TDFTest.java | 98 +++++++++++++++---- 4 files changed, 201 insertions(+), 71 deletions(-) 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 5f241fc0..0e8b044c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -2,12 +2,12 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.TypeAdapter; @@ -107,13 +107,14 @@ public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContex static public class Segment { public String hash; /** - * The plaintext length of this segment. Optional in the JSON: when a producer leaves it - * out it means {@link IntegrityInformation#segmentSizeDefault}, which - * {@link IntegrityInformationAdapterFactory} fills in during deserialization. + * The plaintext length of this segment. Optional when parsing: a producer that leaves the + * key out means {@link IntegrityInformation#segmentSizeDefault}, which is filled in during + * deserialization. Always written on serialization, so a manifest read and re-emitted by + * this SDK carries the value explicitly. */ public long segmentSize; /** - * The on-the-wire length of this segment. Optional in the JSON the same way + * The on-the-wire length of this segment. Optional when parsing the same way * {@link #segmentSize} is, defaulting to * {@link IntegrityInformation#encryptedSegmentSizeDefault}. */ @@ -189,18 +190,24 @@ public int hashCode() { * Applies {@code segmentSizeDefault} / {@code encryptedSegmentSizeDefault} to any segment * that left the corresponding per-segment key out of its JSON. *

- * The per-segment values are optional overrides: {@code manifest.schema.json} marks the two - * defaults required on {@code integrityInformation} but puts no {@code required} list on - * {@code segments/items}. web-sdk omits a per-segment size whenever it equals the default, - * which is every full segment of a payload larger than one segment. Gson leaves an absent + * The per-segment values are optional overrides. As of 2026-09, + * the + * TDF schema lists {@code segmentSizeDefault} and {@code encryptedSegmentSizeDefault} in + * {@code integrityInformation}'s {@code required} array but puts no {@code required} array on + * {@code segments/items}, and web-sdk omits a per-segment size whenever it equals the default + * -- which is every full segment of a payload larger than one segment. Gson leaves an absent * key at {@code 0}, so before this the reader allocated a zero length buffer for those * segments and failed inside the integrity check. *

* This runs as a post-deserialization fixup rather than by boxing the fields to {@code Long}, - * which keeps {@link Segment#segmentSize} a primitive for callers, keeps the value/absence - * distinction out of the public API, and applies to every path that parses a manifest. A - * primitive alone cannot tell an absent key from a literal {@code 0}, so the decision is made - * against the parse tree rather than against the deserialized value. + * which keeps {@link Segment#segmentSize} a primitive for callers and keeps the value/absence + * distinction out of the public API. + *

+ * Two edge cases worth knowing: an explicit {@code null} is treated as absent and gets the + * default, while an explicit {@code 0} is a value and is left alone for the reader to reject. + * And if the manifest omits a default too then there is nothing to fall back to, so + * the segments keep their {@code 0}; {@code TDF.loadTDF} rejects that manifest when it checks + * the two defaults against each other. */ private static class IntegrityInformationAdapterFactory implements TypeAdapterFactory { private static final String SEGMENTS = "segments"; @@ -235,27 +242,35 @@ public T read(JsonReader in) throws IOException { private static void applySegmentSizeDefaults(IntegrityInformation integrityInformation, JsonObject json) { List segments = integrityInformation.segments; JsonElement rawSegments = json.get(SEGMENTS); - if (segments == null || rawSegments == null || !rawSegments.isJsonArray()) { + if (segments == null || rawSegments == null || rawSegments.isJsonNull()) { + // no segments to default; readManifest rejects this with its own message return; } JsonArray rawSegmentArray = rawSegments.getAsJsonArray(); - int count = Math.min(segments.size(), rawSegmentArray.size()); - for (int i = 0; i < count; i++) { + if (segments.size() != rawSegmentArray.size()) { + // Gson's collection adapter is 1:1 with the JSON array, so this cannot happen + // today. asserting it rather than iterating the shorter of the two keeps a future + // filtering adapter from silently leaving the tail segments at 0 + throw new IllegalStateException("internal error: deserialized " + segments.size() + + " segments from a JSON array of " + rawSegmentArray.size()); + } + for (int i = 0; i < segments.size(); i++) { Segment segment = segments.get(i); - JsonElement rawSegment = rawSegmentArray.get(i); - if (segment == null || rawSegment == null || !rawSegment.isJsonObject()) { + if (segment == null) { + // a null entry in the array; readManifest rejects it with its own message continue; } - JsonObject rawSegmentObject = rawSegment.getAsJsonObject(); - if (!hasValue(rawSegmentObject, SEGMENT_SIZE)) { + JsonObject rawSegment = rawSegmentArray.get(i).getAsJsonObject(); + if (!hasValue(rawSegment, SEGMENT_SIZE)) { segment.segmentSize = integrityInformation.segmentSizeDefault; } - if (!hasValue(rawSegmentObject, ENCRYPTED_SEGMENT_SIZE)) { + if (!hasValue(rawSegment, ENCRYPTED_SEGMENT_SIZE)) { segment.encryptedSegmentSize = integrityInformation.encryptedSegmentSizeDefault; } } } + /** Whether {@code memberName} carries a value; an explicit {@code null} counts as absent. */ private static boolean hasValue(JsonObject object, String memberName) { JsonElement member = object.get(memberName); return member != null && !member.isJsonNull(); 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 f18d1ce1..aa4e112e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -420,26 +420,9 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi throw new IllegalStateException("error getting instance of SHA-256", e); } - for (Manifest.Segment segment : manifest.encryptionInformation.integrityInformation.segments) { - if (segment.encryptedSegmentSize <= 0) { - // an encrypted segment always carries at least an IV and a tag, so this only - // happens on a manifest that supplied neither a per-segment - // encryptedSegmentSize nor a usable encryptedSegmentSizeDefault. reported - // here rather than letting a zero length buffer reach the integrity check, - // where it surfaces as an unrelated complaint about the payload being too - // small to GMAC - throw new IllegalStateException("invalid TDF: segment has an encrypted size of " - + segment.encryptedSegmentSize - + ". the manifest supplied neither a per-segment encryptedSegmentSize" - + " nor a usable encryptedSegmentSizeDefault"); - } - - if (segment.encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { - throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " - + Config.MAX_SEGMENT_SIZE); - } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes - // with existing payloads + validateSegmentSizes(manifest); + for (Manifest.Segment segment : manifest.encryptionInformation.integrityInformation.segments) { byte[] readBuf = new byte[(int) segment.encryptedSegmentSize]; int bytesRead = tdfReader.readPayloadBytes(readBuf); @@ -485,6 +468,40 @@ public PolicyObject readPolicyObject() { } } + /** + * Rejects segment sizes that cannot describe a real segment, before any of the payload is + * decrypted. Run as a pre-pass so an invalid size on a later segment does not leave the caller + * holding the plaintext of the earlier ones. + *

+ * A too-small size otherwise reaches the integrity check as an unrelated signature mismatch, + * or under GMAC as a complaint about the payload being too small to hash. A negative one + * reaches {@code new byte[...]} as a {@link NegativeArraySizeException}. + */ + private static void validateSegmentSizes(Manifest manifest) { + // an encrypted segment carries an IV and an auth tag on top of its plaintext, so it can + // never be shorter than the two of them together. an unencrypted payload is stored as-is, + // where the only impossible length is a non-positive one + long minEncryptedSegmentSize = manifest.payload.isEncrypted ? kGcmIvSize + AesGcm.GCM_TAG_LENGTH : 1; + + List segments = manifest.encryptionInformation.integrityInformation.segments; + for (int i = 0; i < segments.size(); i++) { + long encryptedSegmentSize = segments.get(i).encryptedSegmentSize; + + if (encryptedSegmentSize < minEncryptedSegmentSize) { + throw new IllegalStateException("invalid TDF: segment " + i + + " declares an encryptedSegmentSize of " + encryptedSegmentSize + + ", but a segment of this payload cannot be shorter than " + + minEncryptedSegmentSize + " bytes"); + } + + if (encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { + throw new IllegalStateException("Segment size " + encryptedSegmentSize + " exceeded limit " + + Config.MAX_SEGMENT_SIZE); + } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes + // with existing payloads + } + } + private static byte[] calculateSignature(byte[] data, byte[] secret, Config.IntegrityAlgorithm algorithm) { if (algorithm == Config.IntegrityAlgorithm.HS256) { return CryptoUtils.CalculateSHA256Hmac(secret, data); 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 fcc720d6..5e57937d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java @@ -12,6 +12,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class ManifestTest { + private static final long SEGMENT_SIZE_DEFAULT = 1048576; + private static final long ENCRYPTED_SEGMENT_SIZE_DEFAULT = 1048604; + @Test void testManifestMarshalAndUnMarshal() { String kManifestJsonFromTDF = "{\n" + @@ -149,18 +152,21 @@ void testAssertionNull() { assertEquals(manifest.assertions.size(), 0); } - private static final long SEGMENT_SIZE_DEFAULT = 1048576; - private static final long ENCRYPTED_SEGMENT_SIZE_DEFAULT = 1048604; - /** A minimal but valid manifest wrapped around whatever {@code segments} array you give it. */ private static String manifestWithSegments(String segmentsJson) { + return manifestWithSegments(segmentsJson, + " \"encryptedSegmentSizeDefault\": " + ENCRYPTED_SEGMENT_SIZE_DEFAULT + ",\n" + + " \"segmentSizeDefault\": " + SEGMENT_SIZE_DEFAULT + ",\n"); + } + + /** As {@link #manifestWithSegments(String)}, but with the two default declarations spelled out. */ + private static String manifestWithSegments(String segmentsJson, String defaultsJson) { return "{\n" + " \"encryptionInformation\": {\n" + " \"integrityInformation\": {\n" + - " \"encryptedSegmentSizeDefault\": " + ENCRYPTED_SEGMENT_SIZE_DEFAULT + ",\n" + + defaultsJson + " \"rootSignature\": { \"alg\": \"HS256\", \"sig\": \"c2ln\" },\n" + " \"segmentHashAlg\": \"GMAC\",\n" + - " \"segmentSizeDefault\": " + SEGMENT_SIZE_DEFAULT + ",\n" + " \"segments\": [" + segmentsJson + "]\n" + " },\n" + " \"keyAccess\": [ { \"protocol\": \"kas\", \"type\": \"wrapped\"," + @@ -176,10 +182,9 @@ private static String manifestWithSegments(String segmentsJson) { /** * web-sdk leaves {@code segmentSize} and {@code encryptedSegmentSize} out of a segment - * whenever they equal the manifest level defaults. That is legal: {@code manifest.schema.json} - * marks the two defaults required on {@code integrityInformation} but puts no - * {@code required} list on {@code segments/items}, so the per-segment values are optional - * overrides and an absent one means "the default", not zero. + * whenever they equal the manifest level defaults. That is legal, and an absent one means + * "the default" rather than zero -- see {@code IntegrityInformationAdapterFactory} in + * {@link Manifest} for why. */ @Test void testAbsentSegmentSizesFallBackToTheManifestDefaults() { @@ -205,9 +210,11 @@ void testAbsentSegmentSizesFallBackToTheManifestDefaults() { } /** - * An explicit zero is a value rather than an absent key, so it is left alone. The reader - * rejects it with its own error; silently rewriting it to the default would hide a corrupt - * manifest. + * An explicit zero is a value rather than an absent key, so parsing leaves it alone; silently + * rewriting it to the default would hide a corrupt manifest. {@code TDF.Reader} is what + * rejects a zero {@code encryptedSegmentSize}, in + * {@code TDFTest#testZeroLengthSegmentIsRejectedWithAClearError}. A zero {@code segmentSize} + * is not checked anywhere, because nothing on the read path consumes it. */ @Test void testExplicitZeroSegmentSizeIsNotTreatedAsAbsent() { @@ -219,6 +226,35 @@ void testExplicitZeroSegmentSizeIsNotTreatedAsAbsent() { assertThat(segment.encryptedSegmentSize).isZero(); } + /** + * An explicit {@code null} carries no value, so unlike an explicit zero it is treated as + * absent and picks up the default. + */ + @Test + void testNullSegmentSizeIsTreatedAsAbsent() { + Manifest manifest = Manifest.readManifest(manifestWithSegments( + "{ \"hash\": \"aGFzaDA=\", \"segmentSize\": null, \"encryptedSegmentSize\": null }")); + + var segment = manifest.encryptionInformation.integrityInformation.segments.get(0); + assertThat(segment.segmentSize).isEqualTo(SEGMENT_SIZE_DEFAULT); + assertThat(segment.encryptedSegmentSize).isEqualTo(ENCRYPTED_SEGMENT_SIZE_DEFAULT); + } + + /** + * With no defaults declared there is nothing to fall back to, so the segments keep their + * zeroes rather than the fixup inventing a size. {@code TDF.loadTDF} is what rejects such a + * manifest, when it checks the two defaults against each other. + */ + @Test + void testAbsentDefaultsLeaveSegmentSizesAtZero() { + Manifest manifest = Manifest.readManifest( + manifestWithSegments("{ \"hash\": \"aGFzaDA=\" }", "")); + + var segment = manifest.encryptionInformation.integrityInformation.segments.get(0); + assertThat(segment.segmentSize).isZero(); + assertThat(segment.encryptedSegmentSize).isZero(); + } + @Test void testReadingManifestWithObjectStatementValue() throws IOException { final Manifest manifest; diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 172294c2..7647fa49 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -16,6 +16,8 @@ import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; @@ -734,20 +736,30 @@ public void testCreatingTDFWithMultipleSegments() throws Exception { } - /** web-sdk's {@code DEFAULT_SEGMENT_SIZE}, the size at which its omissions start. */ + /** + * A segment size small enough that the test payloads below span several segments. Matches + * web-sdk's {@code DEFAULT_SEGMENT_SIZE}, though nothing here depends on the exact value. + */ private static final int WEB_SDK_DEFAULT_SEGMENT_SIZE = 1024 * 1024; /** * web-sdk drops {@code segmentSize} and {@code encryptedSegmentSize} from a segment whenever * they equal the manifest level defaults, which is every full segment of a payload larger * than one segment. Reproduces that encoding on a java-produced TDF rather than carrying a - * web-sdk fixture, so the test stays self-contained. Without the fallback the reader - * allocates a zero length buffer for those segments and fails inside the integrity check. + * web-sdk fixture, so the test stays self-contained. Before the fallback existed the reader + * allocated a zero length buffer for those segments and failed deep inside the integrity + * check. + * + * @param trailingBytes bytes past the last full segment, so both the "trailing partial + * segment" and the "exact multiple of the segment size" shapes are + * covered. The latter omits both keys on every segment, which is where + * an off-by-one in the fixup's index loop would show up. */ - @Test - public void testReadingATDFThatOmitsDefaultedSegmentSizes() throws Exception { - // two full segments and a partial one, the shape that first exposed this - var data = new byte[2 * WEB_SDK_DEFAULT_SEGMENT_SIZE + 4242]; + @ParameterizedTest + @ValueSource(ints = { 4242, 0 }) + public void testReadingATDFThatOmitsDefaultedSegmentSizes(int trailingBytes) throws Exception { + // two full segments, the shape that first exposed this + var data = new byte[2 * WEB_SDK_DEFAULT_SEGMENT_SIZE + trailingBytes]; new Random(4589).nextBytes(data); var tdf = new TDF(new FakeServicesBuilder().setKas(kas) @@ -768,19 +780,26 @@ public void testReadingATDFThatOmitsDefaultedSegmentSizes() throws Exception { var segmentSizeDefault = integrityInformation.get("segmentSizeDefault").getAsLong(); var encryptedSegmentSizeDefault = integrityInformation.get("encryptedSegmentSizeDefault").getAsLong(); - int omitted = 0; + int omittedSegmentSizes = 0; + int omittedEncryptedSegmentSizes = 0; for (var element : integrityInformation.getAsJsonArray("segments")) { var segment = element.getAsJsonObject(); if (segment.get("segmentSize").getAsLong() == segmentSizeDefault) { segment.remove("segmentSize"); - omitted++; + omittedSegmentSizes++; } if (segment.get("encryptedSegmentSize").getAsLong() == encryptedSegmentSizeDefault) { segment.remove("encryptedSegmentSize"); + omittedEncryptedSegmentSizes++; } } - assertThat(omitted) - .withFailMessage("no segment matched the defaults, so nothing was omitted") + assertThat(omittedSegmentSizes) + .withFailMessage("no segment matched segmentSizeDefault, so nothing was omitted") + .isGreaterThan(0); + // this is the omission the reader actually depends on: plaintext segmentSize is + // write-only in this SDK, so without this assertion the test could go vacuous + assertThat(omittedEncryptedSegmentSizes) + .withFailMessage("no segment matched encryptedSegmentSizeDefault, so nothing was omitted") .isGreaterThan(0); }); @@ -793,12 +812,18 @@ public void testReadingATDFThatOmitsDefaultedSegmentSizes() throws Exception { } /** - * An explicit zero is a corrupt manifest rather than an omitted default, and has to say so - * instead of reaching the integrity check and complaining that the payload is too small to - * GMAC. + * A per-segment {@code encryptedSegmentSize} too small to hold an IV and a tag is a corrupt + * manifest rather than an omitted default, and has to say so instead of reaching the integrity + * check, where it surfaces as a signature mismatch or, under GMAC, as a complaint that the + * payload is too small to hash. A negative one would reach {@code new byte[...]} as a + * {@link NegativeArraySizeException}. + * + * @param encryptedSegmentSize zero; a positive value under the 28 byte IV-plus-tag floor; and + * a negative one */ - @Test - public void testZeroLengthSegmentIsRejectedWithAClearError() throws Exception { + @ParameterizedTest + @ValueSource(ints = { 0, 20, -1 }) + public void testUndersizedSegmentIsRejectedWithAClearError(int encryptedSegmentSize) throws Exception { var data = "some data to encrypt".getBytes(StandardCharsets.UTF_8); var tdf = new TDF(new FakeServicesBuilder().setKas(kas) .setKeyAccessServerRegistryService(kasRegistryService).build()); @@ -813,12 +838,49 @@ public void testZeroLengthSegmentIsRejectedWithAClearError() throws Exception { .getAsJsonObject("integrityInformation") .getAsJsonArray("segments") .get(0).getAsJsonObject() - .addProperty("encryptedSegmentSize", 0)); + .addProperty("encryptedSegmentSize", encryptedSegmentSize)); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(rewritten), platformUrl); assertThatThrownBy(() -> reader.readPayload(new ByteArrayOutputStream())) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("encrypted size of 0"); + .hasMessageContaining("segment 0 declares an encryptedSegmentSize of " + encryptedSegmentSize) + .hasMessageContaining("cannot be shorter than 28 bytes"); + } + + /** + * The size check is a pre-pass, so a bad segment in the middle of a payload fails before any + * of the earlier segments are decrypted. Otherwise the caller is handed truncated plaintext + * from a manifest already known to be invalid. + */ + @Test + public void testUndersizedSegmentIsRejectedBeforeAnyPlaintextIsWritten() throws Exception { + var data = new byte[2 * WEB_SDK_DEFAULT_SEGMENT_SIZE + 4242]; + new Random(4589).nextBytes(data); + + var tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var original = new ByteArrayOutputStream(); + tdf.createTDF(new ByteArrayInputStream(data), original, + Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(WEB_SDK_DEFAULT_SEGMENT_SIZE))); + + // corrupt the *last* segment, so a naive in-loop check would already have emitted the rest + var rewritten = rewriteManifest(original.toByteArray(), manifest -> { + var segments = manifest.getAsJsonObject("encryptionInformation") + .getAsJsonObject("integrityInformation") + .getAsJsonArray("segments"); + segments.get(segments.size() - 1).getAsJsonObject().addProperty("encryptedSegmentSize", 0); + }); + + var unwrapped = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(rewritten), platformUrl); + assertThatThrownBy(() -> reader.readPayload(unwrapped)) + .isInstanceOf(IllegalStateException.class); + assertThat(unwrapped.size()) + .withFailMessage("plaintext was written before the invalid manifest was rejected") + .isZero(); } /** Rebuilds a TDF with its manifest edited in place, leaving the payload bytes untouched. */ From c6462b911eb3acca6938b01a815715ac4e947c5d Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 9 Sep 2026 12:06:17 -0400 Subject: [PATCH 3/3] fix(sdk): DSPX-4589 address SonarCloud findings Move validateSegmentSizes into Reader (S3398) and hoist the output stream out of the assertThatThrownBy lambda (S5778). --- .../java/io/opentdf/platform/sdk/TDF.java | 68 +++++++++---------- .../java/io/opentdf/platform/sdk/TDFTest.java | 3 +- 2 files changed, 36 insertions(+), 35 deletions(-) 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 aa4e112e..c6d6cdf8 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -420,7 +420,7 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi throw new IllegalStateException("error getting instance of SHA-256", e); } - validateSegmentSizes(manifest); + validateSegmentSizes(); for (Manifest.Segment segment : manifest.encryptionInformation.integrityInformation.segments) { byte[] readBuf = new byte[(int) segment.encryptedSegmentSize]; @@ -463,42 +463,42 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi } } - public PolicyObject readPolicyObject() { - return tdfReader.readPolicyObject(); - } - } + /** + * Rejects segment sizes that cannot describe a real segment, before any of the payload is + * decrypted. Run as a pre-pass so an invalid size on a later segment does not leave the + * caller holding the plaintext of the earlier ones. + *

+ * A too-small size otherwise reaches the integrity check as an unrelated signature + * mismatch, or under GMAC as a complaint about the payload being too small to hash. A + * negative one reaches {@code new byte[...]} as a {@link NegativeArraySizeException}. + */ + private void validateSegmentSizes() { + // an encrypted segment carries an IV and an auth tag on top of its plaintext, so it + // can never be shorter than the two of them together. an unencrypted payload is stored + // as-is, where the only impossible length is a non-positive one + long minEncryptedSegmentSize = manifest.payload.isEncrypted ? kGcmIvSize + AesGcm.GCM_TAG_LENGTH : 1; + + List segments = manifest.encryptionInformation.integrityInformation.segments; + for (int i = 0; i < segments.size(); i++) { + long encryptedSegmentSize = segments.get(i).encryptedSegmentSize; + + if (encryptedSegmentSize < minEncryptedSegmentSize) { + throw new IllegalStateException("invalid TDF: segment " + i + + " declares an encryptedSegmentSize of " + encryptedSegmentSize + + ", but a segment of this payload cannot be shorter than " + + minEncryptedSegmentSize + " bytes"); + } - /** - * Rejects segment sizes that cannot describe a real segment, before any of the payload is - * decrypted. Run as a pre-pass so an invalid size on a later segment does not leave the caller - * holding the plaintext of the earlier ones. - *

- * A too-small size otherwise reaches the integrity check as an unrelated signature mismatch, - * or under GMAC as a complaint about the payload being too small to hash. A negative one - * reaches {@code new byte[...]} as a {@link NegativeArraySizeException}. - */ - private static void validateSegmentSizes(Manifest manifest) { - // an encrypted segment carries an IV and an auth tag on top of its plaintext, so it can - // never be shorter than the two of them together. an unencrypted payload is stored as-is, - // where the only impossible length is a non-positive one - long minEncryptedSegmentSize = manifest.payload.isEncrypted ? kGcmIvSize + AesGcm.GCM_TAG_LENGTH : 1; - - List segments = manifest.encryptionInformation.integrityInformation.segments; - for (int i = 0; i < segments.size(); i++) { - long encryptedSegmentSize = segments.get(i).encryptedSegmentSize; - - if (encryptedSegmentSize < minEncryptedSegmentSize) { - throw new IllegalStateException("invalid TDF: segment " + i - + " declares an encryptedSegmentSize of " + encryptedSegmentSize - + ", but a segment of this payload cannot be shorter than " - + minEncryptedSegmentSize + " bytes"); + if (encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { + throw new IllegalStateException("Segment size " + encryptedSegmentSize + " exceeded limit " + + Config.MAX_SEGMENT_SIZE); + } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes + // with existing payloads } + } - if (encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { - throw new IllegalStateException("Segment size " + encryptedSegmentSize + " exceeded limit " - + Config.MAX_SEGMENT_SIZE); - } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes - // with existing payloads + public PolicyObject readPolicyObject() { + return tdfReader.readPolicyObject(); } } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 7647fa49..725ecbd2 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -840,8 +840,9 @@ public void testUndersizedSegmentIsRejectedWithAClearError(int encryptedSegmentS .get(0).getAsJsonObject() .addProperty("encryptedSegmentSize", encryptedSegmentSize)); + var unwrapped = new ByteArrayOutputStream(); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(rewritten), platformUrl); - assertThatThrownBy(() -> reader.readPayload(new ByteArrayOutputStream())) + assertThatThrownBy(() -> reader.readPayload(unwrapped)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("segment 0 declares an encryptedSegmentSize of " + encryptedSegmentSize) .hasMessageContaining("cannot be shorter than 28 bytes");