Skip to content

feat(core): KID in NanoTDF #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 15, 2024
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());
}
}
Loading