Skip to content
4 changes: 3 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public enum ECCurve {
SECP521R1("secp384r1"),
SECP256K1("secp256k1");

private String name;
private final String name;

ECCurve(String curveName) {
this.name = curveName;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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));
Expand All @@ -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) {
Expand All @@ -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);
}
}
13 changes: 10 additions & 3 deletions sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -45,7 +48,7 @@ public String getPublicKey(Config.KASInfo kasInfo) {

@Override
public String getKid(Config.KASInfo kasInfo) {
return "r1";
return KID;
}

@Override
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -50,4 +57,42 @@ void writingResourceLocatorIntoBufferWithInsufficientSize() {
ByteBuffer buffer = ByteBuffer.allocate(1); // Buffer with insufficient size
assertThrows(RuntimeException.class, () -> locator.writeIntoBuffer(buffer));
}
}

@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<Arguments> 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());
}
}