Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -741,14 +741,11 @@ public AssertionConfig.Statement deserialize(JsonElement json, Type typeOfT, Jso
}
var obj = json.getAsJsonObject();
var statement = new AssertionConfig.Statement();
if (obj.has("format")) {
statement.format = obj.get("format").getAsString();
}
if (obj.has("schema")) {
statement.schema = obj.get("schema").getAsString();
}
if (obj.has("value")) {
var value = obj.get("value");
statement.format = optionalString(obj, "format");
statement.schema = optionalString(obj, "schema");
var value = obj.get("value");
// an explicit JSON null is the same as no value at all, not the four character string "null"
if (value != null && !value.isJsonNull()) {
if (value.isJsonPrimitive()) {
// it's already a primitive (hopefully string) so we don't need its escaped value here
statement.value = value.getAsString();
Expand All @@ -758,6 +755,24 @@ public AssertionConfig.Statement deserialize(JsonElement json, Type typeOfT, Jso
}
return statement;
}

/**
* Reads a member that has to be a JSON primitive if present. A missing member and an
* explicit {@code null} both come back as {@code null}; anything else, such as an object
* or array, is a malformed statement. Calling {@code getAsString} unchecked throws
* {@link UnsupportedOperationException} or {@link IllegalStateException} for these, which
* escapes manifest parsing as something other than a parse error.
*/
private static String optionalString(JsonObject obj, String member) {
var element = obj.get(member);
if (element == null || element.isJsonNull()) {
return null;
}
if (!element.isJsonPrimitive()) {
throw new JsonParseException(String.format("assertion statement %s must be a string", member));
}
return element.getAsString();
}
}

public EncryptionInformation encryptionInformation;
Expand Down
17 changes: 14 additions & 3 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ public static boolean isTDF(SeekableByteChannel channel) {
zipReader = new ZipReader(channel);
} catch (IOException | InvalidZipException e) {
return false;
} catch (RuntimeException e) {
// ZipReader reports every malformation it knows about as InvalidZipException, and its
// tests hold it to that. This is a backstop for the contract of this method, which
// screens untrusted input and answers yes or no: a parse failure we did not foresee,
// or an unchecked exception from the caller's channel, still means "not a TDF"
// rather than an exception from the very check meant to spare callers one.
ZipReader.logger.debug("unexpected exception checking whether a channel holds a TDF", e);
return false;
}
var entries = zipReader.getEntries();
var names = entries.stream().map(ZipReader.Entry::getName).collect(Collectors.toSet());
Expand All @@ -191,13 +199,12 @@ public static boolean isTDF(SeekableByteChannel channel) {
* Reads the {@link Manifest} without decrypting the TDF
* @param tdfBytes A SeekableByteChannel containing the TDF data
* @return The parsed {@link Manifest} object
* @throws MalformedTDFException if the bytes are not a well formed TDF or its manifest is invalid
* @throws SDKException if an SDK-specific error occurs
* @throws IOException if an I/O error occurs
*/
public static Manifest readManifest(SeekableByteChannel tdfBytes) throws SDKException, IOException {
TDFReader reader = new TDFReader(tdfBytes);
String manifestJson = reader.manifest();
return Manifest.readManifest(manifestJson);
return TDFReader.open(tdfBytes).readManifest();
}

/**
Expand Down Expand Up @@ -388,6 +395,10 @@ public static class MalformedTDFException extends SDKException {
public MalformedTDFException(String errorMessage) {
super(errorMessage);
}

public MalformedTDFException(String errorMessage, Exception reason) {
super(errorMessage, reason);
}
}

/**
Expand Down
7 changes: 3 additions & 4 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -803,10 +803,9 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig,

Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) throws SDKException, IOException {

TDFReader tdfReader = new TDFReader(tdf);
String manifestJson = tdfReader.manifest();
// use Manifest.readManifest in order to validate the Manifest input
Manifest manifest = Manifest.readManifest(manifestJson);
// a malformed container or manifest is reported as SDK.MalformedTDFException
TDFReader tdfReader = TDFReader.open(tdf);
Manifest manifest = tdfReader.readManifest();

byte[] payloadKey = new byte[GCM_KEY_SIZE];
String unencryptedMetadata = null;
Expand Down
49 changes: 47 additions & 2 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.opentdf.platform.sdk;

import com.google.gson.JsonParseException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -49,10 +51,51 @@ public TDFReader(SeekableByteChannel tdf) throws SDKException, IOException {
payload = entries.get(TDF_PAYLOAD_FILE_NAME).getData();
}

/**
* Opens a TDF read from untrusted bytes, reporting a container that is not a well formed
* TDF as a {@link SDK.MalformedTDFException} rather than as whichever unchecked exception
* the zip reader or this class's own checks happened to raise.
*/
static TDFReader open(SeekableByteChannel tdf) throws IOException {
try {
return new TDFReader(tdf);
} catch (InvalidZipException | IllegalArgumentException e) {
throw malformed("tdf is not a valid zip container", e);
}
}

/**
* Reads and validates the manifest. Malformed JSON, and JSON that does not have the shape of
* a manifest, is reported as a {@link SDK.MalformedTDFException}.
*/
Manifest readManifest() {
String manifestJson = manifest();
try {
return Manifest.readManifest(manifestJson);
} catch (JsonParseException | IllegalArgumentException | IllegalStateException
| UnsupportedOperationException e) {
// Gson reports a JSON value of the wrong type through IllegalStateException and
// UnsupportedOperationException (the JsonElement.getAsX accessors), and
// Manifest.readManifest reports missing required fields as IllegalArgumentException.
// Every one of these here means the manifest itself is malformed.
throw malformed("tdf manifest is invalid", e);
}
}

private static SDK.MalformedTDFException malformed(String what, RuntimeException e) {
return new SDK.MalformedTDFException(what + ": " + e.getMessage(), e);
}

String manifest() {
// deliberately no fixed size cap: very large payloads (DSPX-4502) have manifests well
// over a gigabyte. a hostile declared size cannot make this allocate more than the
// archive holds, because the stream stops at the end of the archive and reports the
// entry as truncated, and ZipReader rejects entries whose stored bytes do not fit
var out = new ByteArrayOutputStream();
try {
manifestEntry.getData().transferTo(out);
} catch (InvalidZipException e) {
throw malformed("error retrieving manifest from zip file", e);
} catch (IOException e) {
throw new SDKException("error retrieving manifest from zip file", e);
}
Expand All @@ -67,15 +110,17 @@ int readPayloadBytes(byte[] buf) {
while (totalRead < buf.length && (nread = payload.read(buf, totalRead, buf.length - totalRead)) >= 0) {
totalRead += nread;
}
} catch (InvalidZipException e) {
// the payload entry runs past the end of the archive
throw malformed("error reading from payload in TDF", e);
} catch (IOException e) {
throw new SDKException("error reading from payload in TDF", e);
}
return totalRead;
}

PolicyObject readPolicyObject() {
String manifestJson = manifest();
Manifest manifest = Manifest.readManifest(manifestJson);
Manifest manifest = readManifest();
return Manifest.decodePolicyObject(manifest);
}
}
Loading
Loading