Skip to content

Commit 33b5982

Browse files
feat(core): KID in NanoTDF (#112)
NanoTDF will now have the KAS KID set in the KAS ResourceLocator Resolves #100 Specification: opentdf/spec#40 ADR: opentdf/platform#900
1 parent 45a2c30 commit 33b5982

File tree

5 files changed

+232
-31
lines changed

5 files changed

+232
-31
lines changed

sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public int createNanoTDF(ByteBuffer data, OutputStream outputStream,
7070
}
7171

7272
// Kas url resource locator
73-
ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL);
73+
ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL, kasInfo.KID);
74+
assert kasURL.getIdentifier() != null : "Identifier in ResourceLocator cannot be null";
75+
7476
ECKeyPair keyPair = new ECKeyPair(nanoTDFConfig.eccMode.getCurveName(), ECKeyPair.ECAlgorithm.ECDSA);
7577

7678
// Generate symmetric key

sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/NanoTDFType.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public enum ECCurve {
77
SECP521R1("secp384r1"),
88
SECP256K1("secp256k1");
99

10-
private String name;
10+
private final String name;
1111

1212
ECCurve(String curveName) {
1313
this.name = curveName;
@@ -18,11 +18,25 @@ public String toString() {
1818
return name;
1919
}
2020
}
21-
21+
// ResourceLocator Protocol
2222
public enum Protocol {
2323
HTTP,
2424
HTTPS
2525
}
26+
// ResourceLocator Identifier
27+
public enum IdentifierType {
28+
NONE(0),
29+
TWO_BYTES(2),
30+
EIGHT_BYTES(8),
31+
THIRTY_TWO_BYTES(32);
32+
private final int length;
33+
IdentifierType(int length) {
34+
this.length = length;
35+
}
36+
public int getLength() {
37+
return length;
38+
}
39+
}
2640

2741
public enum PolicyType {
2842
REMOTE_POLICY,
Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,140 @@
11
package io.opentdf.platform.sdk.nanotdf;
22

33
import java.nio.ByteBuffer;
4-
import java.util.Arrays;
5-
4+
import java.util.Objects;
5+
6+
/**
7+
* The ResourceLocator class represents a resource locator containing a
8+
* protocol, body, and identifier. It provides methods to set and retrieve
9+
* the protocol, body, and identifier, as well as to get the resource URL and
10+
* the total size of the resource locator. It also provides methods to write
11+
* the resource locator into a ByteBuffer and obtain the identifier.
12+
*/
613
public class ResourceLocator {
14+
private static final String HTTP = "http://";
15+
private static final String HTTPS = "https://";
16+
717
private NanoTDFType.Protocol protocol;
818
private int bodyLength;
919
private byte[] body;
20+
private NanoTDFType.IdentifierType identifierType;
21+
private byte[] identifier;
1022

1123
public ResourceLocator() {
1224
}
1325

14-
public ResourceLocator(String resourceUrl) {
15-
if (resourceUrl.startsWith("http://")) {
26+
public ResourceLocator(final String resourceUrl) {
27+
this(resourceUrl, null);
28+
}
29+
30+
/**
31+
* ResourceLocator represents a locator for a resource.
32+
* It takes a resource URL and an identifier as parameters and initializes the object.
33+
* The resource URL is used to determine the protocol and the body.
34+
* The identifier is used to determine the identifier type and the identifier value.
35+
*
36+
* @param resourceUrl the URL of the resource
37+
* @param identifier the identifier of the resource (optional, can be null)
38+
* @throws IllegalArgumentException if the resource URL has an unsupported protocol or if the identifier length is unsupported
39+
*/
40+
public ResourceLocator(final String resourceUrl, final String identifier) {
41+
if (resourceUrl.startsWith(HTTP)) {
1642
this.protocol = NanoTDFType.Protocol.HTTP;
17-
} else if (resourceUrl.startsWith("https://")) {
43+
} else if (resourceUrl.startsWith(HTTPS)) {
1844
this.protocol = NanoTDFType.Protocol.HTTPS;
1945
} else {
20-
throw new RuntimeException("Unsupported protocol for resource locator");
46+
throw new IllegalArgumentException("Unsupported protocol for resource locator");
2147
}
22-
48+
// body
2349
this.body = resourceUrl.substring(resourceUrl.indexOf("://") + 3).getBytes();
2450
this.bodyLength = this.body.length;
51+
// identifier
52+
if (identifier == null) {
53+
this.identifierType = NanoTDFType.IdentifierType.NONE;
54+
this.identifier = new byte[NanoTDFType.IdentifierType.NONE.getLength()];
55+
} else {
56+
int identifierLen = identifier.getBytes().length;
57+
if (identifierLen == 0) {
58+
this.identifierType = NanoTDFType.IdentifierType.NONE;
59+
this.identifier = new byte[NanoTDFType.IdentifierType.NONE.getLength()];
60+
} else if (identifierLen <= 2) {
61+
this.identifierType = NanoTDFType.IdentifierType.TWO_BYTES;
62+
this.identifier = new byte[NanoTDFType.IdentifierType.TWO_BYTES.getLength()];
63+
System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen);
64+
} else if (identifierLen <= 8) {
65+
this.identifierType = NanoTDFType.IdentifierType.EIGHT_BYTES;
66+
this.identifier = new byte[NanoTDFType.IdentifierType.EIGHT_BYTES.getLength()];
67+
System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen);
68+
} else if (identifierLen <= 32) {
69+
this.identifierType = NanoTDFType.IdentifierType.THIRTY_TWO_BYTES;
70+
this.identifier = new byte[NanoTDFType.IdentifierType.THIRTY_TWO_BYTES.getLength()];
71+
System.arraycopy(identifier.getBytes(), 0, this.identifier, 0, identifierLen);
72+
} else {
73+
throw new IllegalArgumentException("Unsupported identifier length: " + identifierLen);
74+
}
75+
}
2576
}
2677

2778
public ResourceLocator(ByteBuffer buffer) {
2879
// Get the first byte and mask it with 0xF to keep only the first four bits
29-
byte protocolByte = buffer.get();
30-
int protocolIndex = protocolByte & 0xF;
31-
this.protocol = NanoTDFType.Protocol.values()[protocolIndex];
80+
final byte protocolWithIdentifier = buffer.get();
81+
int protocolNibble = protocolWithIdentifier & 0x0F;
82+
int identifierNibble = (protocolWithIdentifier & 0xF0) >> 4;
83+
this.protocol = NanoTDFType.Protocol.values()[protocolNibble];
84+
// body
3285
this.bodyLength = buffer.get();
3386
this.body = new byte[this.bodyLength];
3487
buffer.get(this.body);
88+
// identifier
89+
this.identifierType = NanoTDFType.IdentifierType.values()[identifierNibble];
90+
switch (this.identifierType) {
91+
case NONE:
92+
this.identifier = new byte[0];
93+
break;
94+
case TWO_BYTES:
95+
this.identifier = new byte[2];
96+
buffer.get(this.identifier);
97+
break;
98+
case EIGHT_BYTES:
99+
this.identifier = new byte[8];
100+
buffer.get(this.identifier);
101+
break;
102+
case THIRTY_TWO_BYTES:
103+
this.identifier = new byte[32];
104+
buffer.get(this.identifier);
105+
break;
106+
default:
107+
throw new IllegalArgumentException("Unexpected identifier type: " + identifierType);
108+
}
109+
}
110+
111+
public void setIdentifier(String identifier) {
112+
if (identifier == null) {
113+
this.identifierType = NanoTDFType.IdentifierType.NONE;
114+
this.identifier = new byte[0];
115+
} else {
116+
byte[] identifierBytes = identifier.getBytes();
117+
int identifierLen = identifierBytes.length;
118+
119+
if (identifierLen == 0) {
120+
this.identifierType = NanoTDFType.IdentifierType.NONE;
121+
this.identifier = new byte[0];
122+
} else if (identifierLen <= 2) {
123+
this.identifierType = NanoTDFType.IdentifierType.TWO_BYTES;
124+
this.identifier = new byte[2];
125+
System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen);
126+
} else if (identifierLen <= 8) {
127+
this.identifierType = NanoTDFType.IdentifierType.EIGHT_BYTES;
128+
this.identifier = new byte[8];
129+
System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen);
130+
} else if (identifierLen <= 32) {
131+
this.identifierType = NanoTDFType.IdentifierType.THIRTY_TWO_BYTES;
132+
this.identifier = new byte[32];
133+
System.arraycopy(identifierBytes, 0, this.identifier, 0, identifierLen);
134+
} else {
135+
throw new IllegalArgumentException("Unsupported identifier length: " + identifierLen);
136+
}
137+
}
35138
}
36139

37140
public void setProtocol(NanoTDFType.Protocol protocol) {
@@ -49,13 +152,10 @@ public void setBody(byte[] body) {
49152
public String getResourceUrl() {
50153
StringBuilder sb = new StringBuilder();
51154

52-
switch (this.protocol) {
53-
case HTTP:
54-
sb.append("http://");
55-
break;
56-
case HTTPS:
57-
sb.append("https://");
58-
break;
155+
if (Objects.requireNonNull(this.protocol) == NanoTDFType.Protocol.HTTP) {
156+
sb.append(HTTP);
157+
} else if (this.protocol == NanoTDFType.Protocol.HTTPS) {
158+
sb.append(HTTPS);
59159
}
60160

61161
sb.append(new String(this.body));
@@ -64,9 +164,16 @@ public String getResourceUrl() {
64164
}
65165

66166
public int getTotalSize() {
67-
return 1 + 1 + this.body.length;
167+
return 1 + 1 + this.body.length + this.identifier.length;
68168
}
69169

170+
/**
171+
* Writes the resource locator into the provided ByteBuffer.
172+
*
173+
* @param buffer the ByteBuffer to write into
174+
* @return the number of bytes written
175+
* @throws RuntimeException if the buffer size is insufficient to write the resource locator
176+
*/
70177
public int writeIntoBuffer(ByteBuffer buffer) {
71178
int totalSize = getTotalSize();
72179
if (buffer.remaining() < totalSize) {
@@ -76,17 +183,43 @@ public int writeIntoBuffer(ByteBuffer buffer) {
76183
int totalBytesWritten = 0;
77184

78185
// Write the protocol type.
79-
buffer.put((byte) protocol.ordinal());
80-
totalBytesWritten += 1; // size of byte
186+
if (identifierType == NanoTDFType.IdentifierType.NONE) {
187+
buffer.put((byte) protocol.ordinal());
188+
totalBytesWritten += 1; // size of byte
189+
} else {
190+
buffer.put((byte) (identifierType.ordinal() << 4 | protocol.ordinal()));
191+
totalBytesWritten += 1;
192+
}
81193

82-
// Write the url body length;
194+
// Write the url body length
83195
buffer.put((byte)bodyLength);
84196
totalBytesWritten += 1;
85197

86-
// Write the url body;
198+
// Write the url body
87199
buffer.put(body);
88200
totalBytesWritten += body.length;
89201

202+
// Write the identifier
203+
if (identifierType != NanoTDFType.IdentifierType.NONE) {
204+
buffer.put(identifier);
205+
totalBytesWritten += identifier.length;
206+
}
207+
90208
return totalBytesWritten;
91209
}
210+
211+
public byte[] getIdentifier() {
212+
return this.identifier;
213+
}
214+
215+
// getIdentifierString removes potential padding
216+
public String getIdentifierString() {
217+
int actualLength = 0;
218+
for (int i = 0; i < this.identifier.length; i++) {
219+
if (this.identifier[i] != 0) {
220+
actualLength = i + 1;
221+
}
222+
}
223+
return new String(this.identifier, 0, actualLength);
224+
}
92225
}

sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
44
import io.opentdf.platform.sdk.nanotdf.Header;
55
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
6+
import java.nio.charset.StandardCharsets;
67
import org.apache.commons.io.output.ByteArrayOutputStream;
78
import org.junit.jupiter.api.Test;
89

@@ -33,6 +34,8 @@ public class NanoTDFTest {
3334
"oVP7Vpcx\n" +
3435
"-----END PRIVATE KEY-----";
3536

37+
private static final String KID = "r1";
38+
3639
private static SDK.KAS kas = new SDK.KAS() {
3740
@Override
3841
public void close() throws Exception {
@@ -45,7 +48,7 @@ public String getPublicKey(Config.KASInfo kasInfo) {
4548

4649
@Override
4750
public String getKid(Config.KASInfo kasInfo) {
48-
return "r1";
51+
return KID;
4952
}
5053

5154
@Override
@@ -99,6 +102,7 @@ void encryptionAndDecryptionWithValidKey() throws Exception {
99102
var kasInfo = new Config.KASInfo();
100103
kasInfo.URL = "https://api.example.com/kas";
101104
kasInfo.PublicKey = null;
105+
kasInfo.KID = KID;
102106
kasInfos.add(kasInfo);
103107

104108
Config.NanoTDFConfig config = Config.newNanoTDFConfig(
@@ -116,11 +120,14 @@ void encryptionAndDecryptionWithValidKey() throws Exception {
116120

117121
byte[] nanoTDFBytes = tdfOutputStream.toByteArray();
118122
ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream();
123+
nanoTDF = new NanoTDF();
119124
nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas);
120125

121-
String out = new String(plainTextStream.toByteArray(), "UTF-8");
126+
String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8);
122127
assertThat(out).isEqualTo(plainText);
123-
128+
// KAS KID
129+
assertThat(new String(nanoTDFBytes, StandardCharsets.UTF_8)).contains(KID);
130+
124131

125132
int[] nanoTDFSize = { 0, 1, 100*1024, 1024*1024, 4*1024*1024, 12*1024*1024, 15*1024,1024, ((16 * 1024 * 1024) - 3 - 32) };
126133
for (int size: nanoTDFSize) {

sdk/src/test/java/io/opentdf/platform/sdk/nanotdf/ResourceLocatorTest.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package io.opentdf.platform.sdk.nanotdf;
22

3+
import java.nio.ByteBuffer;
4+
import java.util.stream.Stream;
35
import org.junit.jupiter.api.BeforeEach;
46
import org.junit.jupiter.api.Test;
5-
import java.nio.ByteBuffer;
6-
import static org.junit.jupiter.api.Assertions.*;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.Arguments;
9+
import org.junit.jupiter.params.provider.MethodSource;
10+
11+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
714

815
class ResourceLocatorTest {
916
private ResourceLocator locator;
@@ -50,4 +57,42 @@ void writingResourceLocatorIntoBufferWithInsufficientSize() {
5057
ByteBuffer buffer = ByteBuffer.allocate(1); // Buffer with insufficient size
5158
assertThrows(RuntimeException.class, () -> locator.writeIntoBuffer(buffer));
5259
}
53-
}
60+
61+
@ParameterizedTest
62+
@MethodSource("provideUrlsAndIdentifiers")
63+
void creatingResourceLocatorWithDifferentIdentifiers(String url, String identifier, int expectedLength) {
64+
locator = new ResourceLocator(url, identifier);
65+
assertEquals(url, locator.getResourceUrl());
66+
assertEquals(identifier, locator.getIdentifierString());
67+
assertEquals(expectedLength, locator.getIdentifier().length);
68+
}
69+
70+
private static Stream<Arguments> provideUrlsAndIdentifiers() {
71+
return Stream.of(
72+
Arguments.of("http://test.com", "F", 2),
73+
Arguments.of("http://test.com", "e0", 2),
74+
Arguments.of("http://test.com", "e0e0e0e0", 8),
75+
Arguments.of("http://test.com", "e0e0e0e0e0e0e0e0", 32),
76+
Arguments.of("https://test.com", "e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0",32 )
77+
);
78+
}
79+
80+
@Test
81+
void creatingResourceLocatorUnexpectedIdentifierType() {
82+
String url = "http://test.com";
83+
String identifier = "unexpectedIdentifierunexpectedIdentifier";
84+
assertThrows(IllegalArgumentException.class, () -> new ResourceLocator(url, identifier));
85+
}
86+
87+
@Test
88+
void creatingResourceLocatorFromBufferWithIdentifier() {
89+
String url = "http://test.com";
90+
String identifier = "e0";
91+
ResourceLocator original = new ResourceLocator(url, identifier);
92+
byte[] buffer = new byte[original.getTotalSize()];
93+
original.writeIntoBuffer(ByteBuffer.wrap(buffer));
94+
locator = new ResourceLocator(ByteBuffer.wrap(buffer));
95+
assertEquals(url, locator.getResourceUrl());
96+
assertArrayEquals(identifier.getBytes(), locator.getIdentifier());
97+
}
98+
}

0 commit comments

Comments
 (0)