diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java index 6602d143..1da43d54 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java @@ -70,7 +70,9 @@ public int createNanoTDF(ByteBuffer data, OutputStream outputStream, } // Kas url resource locator - ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL); + ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL, kasInfo.KID); + assert kasURL.getIdentifier() != null : "Identifier in ResourceLocator cannot be null"; + ECKeyPair keyPair = new ECKeyPair(nanoTDFConfig.eccMode.getCurveName(), ECKeyPair.ECAlgorithm.ECDSA); // Generate symmetric key diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/NanoTDFType.java b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/NanoTDFType.java index 735d9f76..5e4baaac 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/NanoTDFType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/NanoTDFType.java @@ -7,7 +7,7 @@ public enum ECCurve { SECP521R1("secp384r1"), SECP256K1("secp256k1"); - private String name; + private final String name; ECCurve(String curveName) { this.name = curveName; @@ -18,11 +18,25 @@ public String toString() { return name; } } - + // ResourceLocator Protocol public enum Protocol { HTTP, HTTPS } + // ResourceLocator Identifier + public enum IdentifierType { + NONE(0), + TWO_BYTES(2), + EIGHT_BYTES(8), + THIRTY_TWO_BYTES(32); + private final int length; + IdentifierType(int length) { + this.length = length; + } + public int getLength() { + return length; + } + } public enum PolicyType { REMOTE_POLICY, diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ResourceLocator.java b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ResourceLocator.java index 0244c09a..cf50e1fc 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ResourceLocator.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ResourceLocator.java @@ -1,37 +1,140 @@ package io.opentdf.platform.sdk.nanotdf; import java.nio.ByteBuffer; -import java.util.Arrays; - +import java.util.Objects; + +/** + * The ResourceLocator class represents a resource locator containing a + * protocol, body, and identifier. It provides methods to set and retrieve + * the protocol, body, and identifier, as well as to get the resource URL and + * the total size of the resource locator. It also provides methods to write + * the resource locator into a ByteBuffer and obtain the identifier. + */ public class ResourceLocator { + private static final String HTTP = "http://"; + private static final String HTTPS = "https://"; + private NanoTDFType.Protocol protocol; private int bodyLength; private byte[] body; + private NanoTDFType.IdentifierType identifierType; + private byte[] identifier; public ResourceLocator() { } - public ResourceLocator(String resourceUrl) { - if (resourceUrl.startsWith("http://")) { + public ResourceLocator(final String resourceUrl) { + this(resourceUrl, null); + } + + /** + * ResourceLocator represents a locator for a resource. + * It takes a resource URL and an identifier as parameters and initializes the object. + * The resource URL is used to determine the protocol and the body. + * The identifier is used to determine the identifier type and the identifier value. + * + * @param resourceUrl the URL of the resource + * @param identifier the identifier of the resource (optional, can be null) + * @throws IllegalArgumentException if the resource URL has an unsupported protocol or if the identifier length is unsupported + */ + public ResourceLocator(final String resourceUrl, final String identifier) { + if (resourceUrl.startsWith(HTTP)) { this.protocol = NanoTDFType.Protocol.HTTP; - } else if (resourceUrl.startsWith("https://")) { + } else if (resourceUrl.startsWith(HTTPS)) { this.protocol = NanoTDFType.Protocol.HTTPS; } else { - throw new RuntimeException("Unsupported protocol for resource locator"); + throw new IllegalArgumentException("Unsupported protocol for resource locator"); } - + // body this.body = resourceUrl.substring(resourceUrl.indexOf("://") + 3).getBytes(); this.bodyLength = this.body.length; + // identifier + if (identifier == null) { + this.identifierType = NanoTDFType.IdentifierType.NONE; + this.identifier = new byte[NanoTDFType.IdentifierType.NONE.getLength()]; + } else { + int identifierLen = identifier.getBytes().length; + if (identifierLen == 0) { + this.identifierType = NanoTDFType.IdentifierType.NONE; + this.identifier = new byte[NanoTDFType.IdentifierType.NONE.getLength()]; + } else if (identifierLen <= 2) { + this.identifierType = NanoTDFType.IdentifierType.TWO_BYTES; + this.identifier = new byte[NanoTDFType.IdentifierType.TWO_BYTES.getLength()]; + System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen); + } else if (identifierLen <= 8) { + this.identifierType = NanoTDFType.IdentifierType.EIGHT_BYTES; + this.identifier = new byte[NanoTDFType.IdentifierType.EIGHT_BYTES.getLength()]; + System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen); + } else if (identifierLen <= 32) { + this.identifierType = NanoTDFType.IdentifierType.THIRTY_TWO_BYTES; + this.identifier = new byte[NanoTDFType.IdentifierType.THIRTY_TWO_BYTES.getLength()]; + System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen); + } else { + throw new IllegalArgumentException("Unsupported identifier length: " + identifierLen); + } + } } public ResourceLocator(ByteBuffer buffer) { // Get the first byte and mask it with 0xF to keep only the first four bits - byte protocolByte = buffer.get(); - int protocolIndex = protocolByte & 0xF; - this.protocol = NanoTDFType.Protocol.values()[protocolIndex]; + final byte protocolWithIdentifier = buffer.get(); + int protocolNibble = protocolWithIdentifier & 0x0F; + int identifierNibble = (protocolWithIdentifier & 0xF0) >> 4; + this.protocol = NanoTDFType.Protocol.values()[protocolNibble]; + // body this.bodyLength = buffer.get(); this.body = new byte[this.bodyLength]; buffer.get(this.body); + // identifier + this.identifierType = NanoTDFType.IdentifierType.values()[identifierNibble]; + switch (this.identifierType) { + case NONE: + this.identifier = new byte[0]; + break; + case TWO_BYTES: + this.identifier = new byte[2]; + buffer.get(this.identifier); + break; + case EIGHT_BYTES: + this.identifier = new byte[8]; + buffer.get(this.identifier); + break; + case THIRTY_TWO_BYTES: + this.identifier = new byte[32]; + buffer.get(this.identifier); + break; + default: + throw new IllegalArgumentException("Unexpected identifier type: " + identifierType); + } + } + + public void setIdentifier(String identifier) { + if (identifier == null) { + this.identifierType = NanoTDFType.IdentifierType.NONE; + this.identifier = new byte[0]; + } else { + byte[] identifierBytes = identifier.getBytes(); + int identifierLen = identifierBytes.length; + + if (identifierLen == 0) { + this.identifierType = NanoTDFType.IdentifierType.NONE; + this.identifier = new byte[0]; + } else if (identifierLen <= 2) { + this.identifierType = NanoTDFType.IdentifierType.TWO_BYTES; + this.identifier = new byte[2]; + System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen); + } else if (identifierLen <= 8) { + this.identifierType = NanoTDFType.IdentifierType.EIGHT_BYTES; + this.identifier = new byte[8]; + System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen); + } else if (identifierLen <= 32) { + this.identifierType = NanoTDFType.IdentifierType.THIRTY_TWO_BYTES; + this.identifier = new byte[32]; + System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen); + } else { + throw new IllegalArgumentException("Unsupported identifier length: " + identifierLen); + } + } } public void setProtocol(NanoTDFType.Protocol protocol) { @@ -49,13 +152,10 @@ public void setBody(byte[] body) { public String getResourceUrl() { StringBuilder sb = new StringBuilder(); - switch (this.protocol) { - case HTTP: - sb.append("http://"); - break; - case HTTPS: - sb.append("https://"); - break; + if (Objects.requireNonNull(this.protocol) == NanoTDFType.Protocol.HTTP) { + sb.append(HTTP); + } else if (this.protocol == NanoTDFType.Protocol.HTTPS) { + sb.append(HTTPS); } sb.append(new String(this.body)); @@ -64,9 +164,16 @@ public String getResourceUrl() { } public int getTotalSize() { - return 1 + 1 + this.body.length; + return 1 + 1 + this.body.length + this.identifier.length; } + /** + * Writes the resource locator into the provided ByteBuffer. + * + * @param buffer the ByteBuffer to write into + * @return the number of bytes written + * @throws RuntimeException if the buffer size is insufficient to write the resource locator + */ public int writeIntoBuffer(ByteBuffer buffer) { int totalSize = getTotalSize(); if (buffer.remaining() < totalSize) { @@ -76,17 +183,43 @@ public int writeIntoBuffer(ByteBuffer buffer) { int totalBytesWritten = 0; // Write the protocol type. - buffer.put((byte) protocol.ordinal()); - totalBytesWritten += 1; // size of byte + if (identifierType == NanoTDFType.IdentifierType.NONE) { + buffer.put((byte) protocol.ordinal()); + totalBytesWritten += 1; // size of byte + } else { + buffer.put((byte) (identifierType.ordinal() << 4 | protocol.ordinal())); + totalBytesWritten += 1; + } - // Write the url body length; + // Write the url body length buffer.put((byte)bodyLength); totalBytesWritten += 1; - // Write the url body; + // Write the url body buffer.put(body); totalBytesWritten += body.length; + // Write the identifier + if (identifierType != NanoTDFType.IdentifierType.NONE) { + buffer.put(identifier); + totalBytesWritten += identifier.length; + } + return totalBytesWritten; } + + public byte[] getIdentifier() { + return this.identifier; + } + + // getIdentifierString removes potential padding + public String getIdentifierString() { + int actualLength = 0; + for (int i = 0; i < this.identifier.length; i++) { + if (this.identifier[i] != 0) { + actualLength = i + 1; + } + } + return new String(this.identifier, 0, actualLength); + } } \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java index e0aec19f..960f1b02 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java @@ -3,6 +3,7 @@ import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.Header; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; +import java.nio.charset.StandardCharsets; import org.apache.commons.io.output.ByteArrayOutputStream; import org.junit.jupiter.api.Test; @@ -33,6 +34,8 @@ public class NanoTDFTest { "oVP7Vpcx\n" + "-----END PRIVATE KEY-----"; + private static final String KID = "r1"; + private static SDK.KAS kas = new SDK.KAS() { @Override public void close() throws Exception { @@ -45,7 +48,7 @@ public String getPublicKey(Config.KASInfo kasInfo) { @Override public String getKid(Config.KASInfo kasInfo) { - return "r1"; + return KID; } @Override @@ -99,6 +102,7 @@ void encryptionAndDecryptionWithValidKey() throws Exception { var kasInfo = new Config.KASInfo(); kasInfo.URL = "https://api.example.com/kas"; kasInfo.PublicKey = null; + kasInfo.KID = KID; kasInfos.add(kasInfo); Config.NanoTDFConfig config = Config.newNanoTDFConfig( @@ -116,11 +120,14 @@ void encryptionAndDecryptionWithValidKey() throws Exception { byte[] nanoTDFBytes = tdfOutputStream.toByteArray(); ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream(); + nanoTDF = new NanoTDF(); nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas); - String out = new String(plainTextStream.toByteArray(), "UTF-8"); + String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8); assertThat(out).isEqualTo(plainText); - + // KAS KID + assertThat(new String(nanoTDFBytes, StandardCharsets.UTF_8)).contains(KID); + int[] nanoTDFSize = { 0, 1, 100*1024, 1024*1024, 4*1024*1024, 12*1024*1024, 15*1024,1024, ((16 * 1024 * 1024) - 3 - 32) }; for (int size: nanoTDFSize) { diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/nanotdf/ResourceLocatorTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/nanotdf/ResourceLocatorTest.java index a5c16b8f..ec6a17c8 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/nanotdf/ResourceLocatorTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/nanotdf/ResourceLocatorTest.java @@ -1,9 +1,16 @@ package io.opentdf.platform.sdk.nanotdf; +import java.nio.ByteBuffer; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.nio.ByteBuffer; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class ResourceLocatorTest { private ResourceLocator locator; @@ -50,4 +57,42 @@ void writingResourceLocatorIntoBufferWithInsufficientSize() { ByteBuffer buffer = ByteBuffer.allocate(1); // Buffer with insufficient size assertThrows(RuntimeException.class, () -> locator.writeIntoBuffer(buffer)); } -} \ No newline at end of file + + @ParameterizedTest + @MethodSource("provideUrlsAndIdentifiers") + void creatingResourceLocatorWithDifferentIdentifiers(String url, String identifier, int expectedLength) { + locator = new ResourceLocator(url, identifier); + assertEquals(url, locator.getResourceUrl()); + assertEquals(identifier, locator.getIdentifierString()); + assertEquals(expectedLength, locator.getIdentifier().length); + } + + private static Stream provideUrlsAndIdentifiers() { + return Stream.of( + Arguments.of("http://test.com", "F", 2), + Arguments.of("http://test.com", "e0", 2), + Arguments.of("http://test.com", "e0e0e0e0", 8), + Arguments.of("http://test.com", "e0e0e0e0e0e0e0e0", 32), + Arguments.of("https://test.com", "e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0",32 ) + ); + } + + @Test + void creatingResourceLocatorUnexpectedIdentifierType() { + String url = "http://test.com"; + String identifier = "unexpectedIdentifierunexpectedIdentifier"; + assertThrows(IllegalArgumentException.class, () -> new ResourceLocator(url, identifier)); + } + + @Test + void creatingResourceLocatorFromBufferWithIdentifier() { + String url = "http://test.com"; + String identifier = "e0"; + ResourceLocator original = new ResourceLocator(url, identifier); + byte[] buffer = new byte[original.getTotalSize()]; + original.writeIntoBuffer(ByteBuffer.wrap(buffer)); + locator = new ResourceLocator(ByteBuffer.wrap(buffer)); + assertEquals(url, locator.getResourceUrl()); + assertArrayEquals(identifier.getBytes(), locator.getIdentifier()); + } +}