Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 88 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# OpenTDF Java SDK

A Java implementation of the OpenTDF protocol, and access library for the services provided by the OpenTDF platform.

**New to the OpenTDF SDK?** See the [OpenTDF SDK Quickstart Guide](https://opentdf.io/category/sdk) for a comprehensive introduction.

This SDK is available from Maven central as:

```xml
Expand All @@ -14,50 +17,111 @@ This SDK is available from Maven central as:

## Quick Start Example

This example demonstrates how to create and read TDF (Trusted Data Format) files using the OpenTDF SDK.

**Prerequisites:** Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to get a local platform running, or if you already have a hosted version, replace the values with your OpenTDF platform details.

For more code examples, see:
- [Creating TDFs](https://opentdf.io/sdks/tdf)
- [Managing policy](https://opentdf.io/sdks/policy)

```java
package io.opentdf.platform;

import io.opentdf.platform.sdk.Config;
import io.opentdf.platform.sdk.Reader;
import io.opentdf.platform.sdk.SDK;
import io.opentdf.platform.sdk.SDKBuilder;
import io.opentdf.platform.sdk.TDF;

import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Example {
public static void main(String[] args) throws IOException {
public static void main(String[] args) throws Exception {
// Initialize SDK with platform endpoint and authentication
// Replace these values with your actual configuration:
String platformEndpoint = "localhost:8080"; // Your platform URL
String clientId = "opentdf"; // Your OAuth client ID
String clientSecret = "secret"; // Your OAuth client secret
String kasUrl = "http://localhost:8080/kas"; // Your KAS URL

SDK sdk = new SDKBuilder()
.clientSecret("myClient", "token")
.platformEndpoint("https://your.cluster/")
.build();

// Fetch the platform base key (if configured)
sdk.getBaseKey().ifPresent(baseKey -> {
System.out.println(baseKey.getKasUri());
System.out.println(baseKey.getPublicKey().getKid());
});

// Encrypt a file
try (InputStream in = new FileInputStream("input.plaintext")) {
Config.TDFConfig c = Config.newTDFConfig(Config.withDataAttributes("attr1", "attr2"));
sdk.createTDF(in, System.out, c);
.platformEndpoint(platformEndpoint)
.clientSecret(clientId, clientSecret)
.useInsecurePlaintextConnection(true) // Only for local development with HTTP
.build();

// Create a TDF
// This attribute is created in the quickstart guide
String dataAttribute = "https://opentdf.io/attr/department/value/finance";

String plaintext = "Hello, world!";
var plaintextInputStream = new ByteArrayInputStream(plaintext.getBytes(StandardCharsets.UTF_8));

var kasInfo = new Config.KASInfo();
kasInfo.URL = kasUrl;

var tdfConfig = Config.newTDFConfig(
Config.withKasInformation(kasInfo),
Config.withDataAttributes(dataAttribute)
);

// Write encrypted TDF to file
try (FileOutputStream out = new FileOutputStream("encrypted.tdf")) {
sdk.createTDF(plaintextInputStream, out, tdfConfig);
}

// Decrypt a file
try (SeekableByteChannel in = FileChannel.open(Path.of("input.ciphertext"), StandardOpenOption.READ)) {
Reader reader = sdk.loadTDF(in, Config.newTDFReaderConfig());
reader.readPayload(System.out);
System.out.println("TDF created successfully");

// Decrypt the TDF
// LoadTDF contacts the Key Access Service (KAS) to verify that this client
// has been granted access to the data attributes, then decrypts the TDF.
// Note: The client must have entitlements configured on the platform first.
Path tdfPath = Paths.get("encrypted.tdf");
try (FileChannel tdfChannel = FileChannel.open(tdfPath, StandardOpenOption.READ)) {
TDF.Reader reader = sdk.loadTDF(tdfChannel, Config.newTDFReaderConfig());

// Write the decrypted plaintext to a file
try (FileOutputStream out = new FileOutputStream("output.txt")) {
reader.readPayload(out);
}
}

System.out.println("Successfully created and decrypted TDF");
}
}
```

### Configuration Values

Replace these placeholder values with your actual configuration:

| Variable | Default (Quickstart) | Description |
|----------|---------------------|-------------|
| `platformEndpoint` | `localhost:8080` | Your OpenTDF platform URL |
| `clientId` | `opentdf` | OAuth client ID (from quickstart) |
| `clientSecret` | `secret` | OAuth client secret (from quickstart) |
| `kasUrl` | `http://localhost:8080/kas` | Your Key Access Service URL |
| `dataAttribute` | `https://opentdf.io/attr/department/value/finance` | Data attribute FQN (created in quickstart) |

**Before running:**
1. Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to start the platform
2. Create an OAuth client in Keycloak and note the credentials
3. Grant your client entitlements to the `department` attribute (see [Managing policy](https://opentdf.io/sdks/policy))

**Expected Output:**
```
TDF created successfully
Successfully created and decrypted TDF
```

The `output.txt` file will contain the decrypted plaintext: `Hello, world!`

### Cryptography Library

This SDK uses the [Bouncy Castle Security library](https://www.bouncycastle.org/) library.
Expand Down
135 changes: 135 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/READMETest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package io.opentdf.platform.sdk;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.*;

/**
* READMETest verifies that Java code blocks in the README are syntactically valid.
* This ensures the documentation stays accurate and up-to-date with the actual API.
*/
public class READMETest {

// Pre-compiled pattern for extracting Java code blocks from markdown
private static final Pattern JAVA_CODE_BLOCK_PATTERN = Pattern.compile("```java\\n(.*?)```", Pattern.DOTALL);

@Test
public void testREADMECodeBlocks() throws IOException {
// Read the README file
Path readmePath = Path.of("..").resolve("README.md").toAbsolutePath().normalize();
String content = Files.readString(readmePath);

// Extract Java code blocks
List<String> codeBlocks = extractJavaCodeBlocks(content);
assertTrue(codeBlocks.size() > 0, "No Java code blocks found in README.md");

System.out.println("Found " + codeBlocks.size() + " Java code block(s) in README.md");

// Test each code block that is a complete program
int testedCount = 0;
for (int i = 0; i < codeBlocks.size(); i++) {
String code = codeBlocks.get(i);

// Only test complete programs (those with package and main method)
if (!code.contains("package ") || !code.contains("public static void main")) {
System.out.println("Skipping code block " + (i + 1) + " (not a complete program)");
continue;
}

testedCount++;
System.out.println("Testing code block " + (i + 1));

try {
validateCodeBlock(code, i + 1);
System.out.println("Code block " + (i + 1) + " validated successfully");
} catch (AssertionError e) {
fail("Code block " + (i + 1) + " validation failed: " + e.getMessage());
}
}

assertTrue(testedCount > 0, "No complete program code blocks found in README.md");
System.out.println("Validated " + testedCount + " complete program(s)");
}

/**
* Extracts all Java code blocks from the markdown content.
*/
private List<String> extractJavaCodeBlocks(String content) {
List<String> blocks = new ArrayList<>();
Matcher matcher = JAVA_CODE_BLOCK_PATTERN.matcher(content);

while (matcher.find()) {
blocks.add(matcher.group(1));
}

return blocks;
}

/**
* Validates that a code block has proper structure and imports.
*/
private void validateCodeBlock(String code, int blockNumber) {
// Check for required imports that should be present
assertTrue(code.contains("import io.opentdf.platform.sdk.SDK"),
"Block " + blockNumber + ": Missing SDK import");
assertTrue(code.contains("import io.opentdf.platform.sdk.SDKBuilder"),
"Block " + blockNumber + ": Missing SDKBuilder import");
assertTrue(code.contains("import io.opentdf.platform.sdk.Config"),
"Block " + blockNumber + ": Missing Config import");

// Check that Reader is imported as TDF not as standalone Reader
assertFalse(code.contains("import io.opentdf.platform.sdk.Reader"),
"Block " + blockNumber + ": Should not import non-existent Reader class. Use TDF.Reader instead.");

// Check for proper TDF.Reader usage (not just Reader)
if (code.contains("Reader reader")) {
assertTrue(code.contains("TDF.Reader reader") || code.contains("import io.opentdf.platform.sdk.TDF"),
"Block " + blockNumber + ": Should use TDF.Reader, not Reader");
}

// Check for correct API usage
if (code.contains("loadTDF")) {
assertTrue(code.contains("sdk.loadTDF"),
"Block " + blockNumber + ": loadTDF should be called on SDK instance");
}

if (code.contains("createTDF")) {
assertTrue(code.contains("sdk.createTDF"),
"Block " + blockNumber + ": createTDF should be called on SDK instance");
}

// Check for proper class structure
assertTrue(code.contains("public class"),
"Block " + blockNumber + ": Should contain a public class");
assertTrue(code.contains("public static void main(String[] args)") ||
code.contains("public static void main(String... args)"),
"Block " + blockNumber + ": Should contain a main method");

// Check for proper exception handling
assertTrue(code.contains("throws") || code.contains("try"),
"Block " + blockNumber + ": Should handle exceptions (throws or try-catch)");

// Validate matching braces
long openBraces = code.chars().filter(ch -> ch == '{').count();
long closeBraces = code.chars().filter(ch -> ch == '}').count();
assertEquals(openBraces, closeBraces,
"Block " + blockNumber + ": Mismatched braces");

// Validate matching parentheses
long openParens = code.chars().filter(ch -> ch == '(').count();
long closeParens = code.chars().filter(ch -> ch == ')').count();
assertEquals(openParens, closeParens,
"Block " + blockNumber + ": Mismatched parentheses");

System.out.println(" ✓ All validation checks passed");
}
}

Loading