Skip to content
Merged
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
25 changes: 17 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 @@ -1141,14 +1141,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");
// explicit JSON null means no value, not the 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 @@ -1158,6 +1155,18 @@ public AssertionConfig.Statement deserialize(JsonElement json, Type typeOfT, Jso
}
return statement;
}

/** The member as a string, or null if absent or JSON null; a non-primitive is a JsonParseException. */
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
15 changes: 11 additions & 4 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public static boolean isTDF(SeekableByteChannel channel) {
zipReader = new ZipReader(channel);
} catch (IOException | InvalidZipException e) {
return false;
} catch (RuntimeException e) {
// backstop for the yes-or-no contract: an unforeseen parse failure, or an unchecked
// exception from the caller's channel, still means "not a TDF"
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 @@ -218,14 +223,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);
try (var manifestJson = reader.manifest()) {
return Manifest.readManifest(manifestJson);
}
return TDFReader.open(tdfBytes).readManifest();
}

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

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

/**
Expand Down
8 changes: 2 additions & 6 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -848,12 +848,8 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig,

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

TDFReader tdfReader = new TDFReader(tdf);
// use Manifest.readManifest in order to validate the Manifest input
Manifest manifest;
try (var manifestJson = tdfReader.manifest()) {
manifest = Manifest.readManifest(manifestJson);
}
TDFReader tdfReader = TDFReader.open(tdf);
Manifest manifest = tdfReader.readManifest();

byte[] payloadKey = new byte[GCM_KEY_SIZE];
String unencryptedMetadata = null;
Expand Down
56 changes: 49 additions & 7 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.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -54,29 +56,69 @@ public TDFReader(SeekableByteChannel tdf) throws SDKException, IOException {
payloadEntry = entries.get(TDF_PAYLOAD_FILE_NAME);
}

/** Like the constructor, but reports a malformed container as {@link SDK.MalformedTDFException}. */
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() {
try (Reader manifestJson = manifest()) {
return Manifest.readManifest(manifestJson);
} catch (InvalidZipException e) {
// the manifest entry runs past the end of the archive
throw malformed("error retrieving manifest from zip file", e);
} catch (JsonParseException | IllegalArgumentException e) {
// Manifest.readManifest reports a missing or misshapen field as IllegalArgumentException
throw malformed("tdf manifest is invalid", e);
} catch (IOException e) {
throw new SDKException("error reading manifest from zip file", e);
}
}

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

/**
* The manifest entry as a character stream; the caller must close it. Returned
* as a stream rather than a String because a manifest with tens of millions of
* segments exceeds the maximum size of a Java String.
*/
Reader manifest() {
// no fixed size cap: manifests for very large payloads exceed 1 GiB. a hostile declared
// size is still bounded by the archive, since the entry's stream throws where it ends
try {
return new BufferedReader(
new InputStreamReader(manifestEntry.getData(), StandardCharsets.UTF_8), MANIFEST_BUFFER_SIZE);
} 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);
}
}

/** A fresh stream over the payload, positioned at its start. */
InputStream payload() throws IOException {
return payloadEntry.getData();
try {
return payloadEntry.getData();
} catch (InvalidZipException e) {
// the payload's local header puts its data past the end of the archive
throw malformed("error reading from payload in TDF", e);
}
}

int readPayloadBytes(byte[] buf) {
if (payload == null) {
try {
payload = payloadEntry.getData();
payload = payload();
} catch (IOException e) {
throw new SDKException("error reading from payload in TDF", e);
}
Expand All @@ -91,17 +133,17 @@ static int readPayloadBytes(InputStream payload, 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() {
try (Reader manifestRaw = manifest()) {
return Manifest.decodePolicyObject(Manifest.readManifest(manifestRaw));
} catch (IOException e) {
throw new SDKException("error reading manifest from zip file", e);
}
Manifest manifest = readManifest();
return Manifest.decodePolicyObject(manifest);
}
}
Loading
Loading