diff --git a/README.md b/README.md index fc24ae39..3ad9bdb3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/READMETest.java b/sdk/src/test/java/io/opentdf/platform/sdk/READMETest.java new file mode 100644 index 00000000..081ef882 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/READMETest.java @@ -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 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 extractJavaCodeBlocks(String content) { + List 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"); + } +} +