diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java b/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java new file mode 100644 index 00000000..e8ac4648 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java @@ -0,0 +1,7 @@ +package io.opentdf.platform.sdk; + +public class InvalidZipException extends RuntimeException { + public InvalidZipException(String message) { + super(message); + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java index df94dc59..426b6c3b 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -13,8 +13,12 @@ import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc; import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc.SubjectMappingServiceFutureStub; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; import java.util.Optional; /** @@ -26,6 +30,8 @@ public class SDK implements AutoCloseable { private final TrustManager trustManager; private final ClientInterceptor authInterceptor; + private static final Logger log = LoggerFactory.getLogger(SDK.class); + @Override public void close() throws Exception { services.close(); @@ -112,4 +118,25 @@ public Optional getAuthInterceptor() { public Services getServices() { return this.services; } + + /** + * Checks to see if this has the structure of a Z-TDF in that it is a zip file containing + * a `manifest.json` and a `0.payload` + * @param channel A channel containing the bytes of the potential Z-TDF + * @return `true` if + */ + public static boolean isTDF(SeekableByteChannel channel) { + ZipReader zipReader; + try { + zipReader = new ZipReader(channel); + } catch (IOException | InvalidZipException e) { + return false; + } + var entries = zipReader.getEntries(); + if (entries.size() != 2) { + return false; + } + return entries.stream().anyMatch(e -> "0.manifest.json".equals(e.getName())) + && entries.stream().anyMatch(e -> "0.payload".equals(e.getName())); + } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java b/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java index cc9d8d1e..17bc51c5 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java @@ -84,7 +84,7 @@ CentralDirectoryRecord readEndOfCentralDirectory() throws IOException { } if (eoCDRStart < 0) { - throw new RuntimeException("Didn't find the end of central directory"); + throw new InvalidZipException("Didn't find the end of central directory"); } short diskNumber = readShort(); @@ -109,7 +109,7 @@ private CentralDirectoryRecord extractZIP64CentralDirectoryInfo() throws IOExcep // buffer's position at the start of the Central Directory int signature = readInt(); if (signature != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE) { - throw new RuntimeException("Invalid Zip64 End of Central Directory Record Signature"); + throw new InvalidZipException("Invalid Zip64 End of Central Directory Record Signature"); } int centralDirectoryDiskNumber = readInt(); @@ -119,7 +119,7 @@ private CentralDirectoryRecord extractZIP64CentralDirectoryInfo() throws IOExcep zipChannel.position(offsetToEndOfCentralDirectory); int sig = readInt(); if (sig != ZIP_64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) { - throw new RuntimeException("Invalid"); + throw new InvalidZipException("Invalid"); } long sizeOfEndOfCentralDirectoryRecord = readLong(); short versionMadeBy = readShort(); @@ -152,7 +152,7 @@ public String getName() { public InputStream getData() throws IOException { zipChannel.position(offsetToLocalHeader); if (readInt() != LOCAL_FILE_HEADER_SIGNATURE) { - throw new RuntimeException("Invalid Local Header Signature"); + throw new InvalidZipException("Invalid Local Header Signature"); } zipChannel.position(zipChannel.position() + Short.BYTES @@ -218,7 +218,7 @@ public int read(byte[] b, int off, int len) throws IOException { public Entry readCentralDirectoryFileHeader() throws IOException { int signature = readInt(); if (signature != CENTRAL_FILE_HEADER_SIGNATURE) { - throw new RuntimeException("Invalid Central Directory File Header Signature"); + throw new InvalidZipException("Invalid Central Directory File Header Signature"); } short versionMadeBy = readShort(); short versionNeededToExtract = readShort(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java new file mode 100644 index 00000000..fda5705b --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java @@ -0,0 +1,78 @@ +package io.opentdf.platform.sdk; + +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +class SDKTest { + + @Test + void testExaminingValidZTDF() throws IOException { + try (var ztdf = SDKTest.class.getClassLoader().getResourceAsStream("sample.txt.tdf")) { + assert ztdf != null; + var chan = new SeekableInMemoryByteChannel(ztdf.readAllBytes()); + assertThat(SDK.isTDF(chan)).isTrue(); + } + } + + @Test + void testExaminingInvalidFile() { + var chan = new SeekableByteChannel() { + @Override + public boolean isOpen() { + return false; + } + + @Override + public void close() { + + } + + @Override + public int read(ByteBuffer dst) { + return 0; + } + + @Override + public int write(ByteBuffer src) { + return 0; + } + + @Override + public long position() { + return 0; + } + + @Override + public SeekableByteChannel position(long newPosition) { + return this; + } + + @Override + public long size() { + return 0; + } + + @Override + public SeekableByteChannel truncate(long size) { + return null; + } + }; + + assertThat(SDK.isTDF(chan)).isFalse(); + } + + @Test + void testReadingRandomBytes() { + var tdf = new byte[2023]; + new Random().nextBytes(tdf); + + assertThat(SDK.isTDF(new SeekableInMemoryByteChannel(tdf))).isFalse(); + } +} \ No newline at end of file