diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index fa06679e..91e4e99b 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -42,12 +42,48 @@ jobs: distribution: "adopt" server-id: github - name: Maven Verify - run: | - mvn --batch-mode verify + run: mvn --batch-mode verify env: BUF_INPUT_HTTPS_USERNAME: opentdf-bot BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.PERSONAL_ACCESS_TOKEN_OPENTDF }} + sonarcloud: + name: SonarCloud Scan + runs-on: ubuntu-22.04 + steps: + - name: Check out repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + fetch-depth: 0 + - uses: bufbuild/buf-setup-action@2211e06e8cf26d628cda2eea15c95f8c42b080b3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up JDK + uses: actions/setup-java@5896cecc08fd8a1fbdfaf517e29b571164b031f7 + with: + java-version: "17" + distribution: "temurin" + server-id: github + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Maven Test Coverage + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUF_INPUT_HTTPS_USERNAME: opentdf-bot + BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.PERSONAL_ACCESS_TOKEN_OPENTDF }} + run: mvn --batch-mode clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=opentdf_java-sdk -P coverage + platform-integration: runs-on: ubuntu-22.04 steps: @@ -257,6 +293,7 @@ jobs: - platform-integration - platform-xtest - mavenverify + - sonarcloud - pr runs-on: ubuntu-latest if: always() diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 00000000..755b2183 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,45 @@ +name: "CodeQL" + +on: + schedule: + - cron: '0 13 * * 1' # At 1:00 PM UTC every Monday + pull_request: + paths: + - '.github/workflows/codeql.yaml' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Buf setup + uses: bufbuild/buf-setup-action@2211e06e8cf26d628cda2eea15c95f8c42b080b3 + + - name: Initialize the CodeQL tools for scanning + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + timeout-minutes: 5 + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + timeout-minutes: 10 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + timeout-minutes: 10 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 88ca3ece..5a8dc58d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.3" + ".": "0.7.5" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d54306d4..59e03bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [0.7.5](https://github.com/opentdf/java-sdk/compare/v0.7.4...v0.7.5) (2024-10-29) + + +### Features + +* Examples module ([#202](https://github.com/opentdf/java-sdk/issues/202)) ([ac13a0a](https://github.com/opentdf/java-sdk/commit/ac13a0a7c82caed920238244cf7adaca3039fdea)) + + +### Bug Fixes + +* **sdk:** option to disable assertion verification ([#205](https://github.com/opentdf/java-sdk/issues/205)) ([78d7b66](https://github.com/opentdf/java-sdk/commit/78d7b66e40bb52340e604ab645830287c91ba534)) + +## [0.7.4](https://github.com/opentdf/java-sdk/compare/v0.7.3...v0.7.4) (2024-10-24) + + +### Bug Fixes + +* **sdk:** returns the correct string associated with enums ([#200](https://github.com/opentdf/java-sdk/issues/200)) ([1dffd35](https://github.com/opentdf/java-sdk/commit/1dffd35374c40ebaa095594d2a5db138957c6e38)) + + +### Documentation + +* JavaDoc ([#196](https://github.com/opentdf/java-sdk/issues/196)) ([33c9513](https://github.com/opentdf/java-sdk/commit/33c9513de68954cccba854d501ba26b62216df89)) +* minor Java SDK README updates ([#193](https://github.com/opentdf/java-sdk/issues/193)) ([e9dc738](https://github.com/opentdf/java-sdk/commit/e9dc738cc40ffc97d3f0084086b1afa1c283850c)) + ## [0.7.3](https://github.com/opentdf/java-sdk/compare/v0.7.2...v0.7.3) (2024-10-09) diff --git a/cmdline/pom.xml b/cmdline/pom.xml index 705455f6..bc33e5cc 100644 --- a/cmdline/pom.xml +++ b/cmdline/pom.xml @@ -4,7 +4,7 @@ io.opentdf.platform sdk-pom - 0.7.4-SNAPSHOT + 0.7.5 cmdline diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index f8b37d0e..513c226b 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -1,9 +1,11 @@ package io.opentdf.platform; +import com.google.gson.JsonSyntaxException; import com.nimbusds.jose.JOSEException; import io.opentdf.platform.sdk.*; import io.opentdf.platform.sdk.TDF; +import com.google.gson.Gson; import org.apache.commons.codec.DecoderException; import picocli.CommandLine; import picocli.CommandLine.HelpCommand; @@ -64,7 +66,9 @@ void encrypt( @Option(names = { "-a", "--attr" }, defaultValue = Option.NULL_VALUE) Optional attributes, @Option(names = { "-c", "--autoconfigure" }, defaultValue = Option.NULL_VALUE) Optional autoconfigure, - @Option(names = { "--mime-type" }, defaultValue = Option.NULL_VALUE) Optional mimeType) + @Option(names = { "--mime-type" }, defaultValue = Option.NULL_VALUE) Optional mimeType, + @Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional assertion) + throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException { var sdk = buildSDK(); @@ -79,6 +83,21 @@ void encrypt( metadata.map(Config::withMetaData).ifPresent(configs::add); autoconfigure.map(Config::withAutoconfigure).ifPresent(configs::add); mimeType.map(Config::withMimeType).ifPresent(configs::add); + + if (assertion.isPresent()) { + var assertionConfig = assertion.get(); + Gson gson = new Gson(); + + AssertionConfig[] assertionConfigs; + try { + assertionConfigs = gson.fromJson(assertionConfig, AssertionConfig[].class); + } catch (JsonSyntaxException e) { + throw new RuntimeException("Failed to parse assertion, expects an list of assertions", e); + } + + configs.add(Config.withAssertionConfig(assertionConfigs)); + } + if (attributes.isPresent()) { configs.add(Config.withDataAttributes(attributes.get().split(","))); } diff --git a/examples/buf.gen.yaml b/examples/buf.gen.yaml new file mode 100644 index 00000000..6bb9c4e3 --- /dev/null +++ b/examples/buf.gen.yaml @@ -0,0 +1,14 @@ +version: v1 +managed: + enabled: true + java_package_prefix: + default: io.opentdf.platform + except: + - buf.build/bufbuild/protovalidate + - buf.build/googleapis/googleapis + - buf.build/grpc-ecosystem/grpc-gateway +plugins: + - plugin: buf.build/protocolbuffers/java:v25.3 + out: ./ + - plugin: buf.build/grpc/java:v1.61.1 + out: ./ diff --git a/examples/buf.lock b/examples/buf.lock new file mode 100644 index 00000000..deef61e8 --- /dev/null +++ b/examples/buf.lock @@ -0,0 +1,18 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: bufbuild + repository: protovalidate + commit: f05a6f4403ce4327bae4f50f281c3ed0 + digest: shake256:668a0661b8df44d41839194896329330965fc215f3d2f88057fd60eeb759c2daf6cc6edfdd13b2a653d49fe2896ebedcb1a33c4c5b2dd10919f03ffb7fc52ae6 + - remote: buf.build + owner: googleapis + repository: googleapis + commit: 7e6f6e774e29406da95bd61cdcdbc8bc + digest: shake256:fe43dd2265ea0c07d76bd925eeba612667cf4c948d2ce53d6e367e1b4b3cb5fa69a51e6acb1a6a50d32f894f054a35e6c0406f6808a483f2752e10c866ffbf73 + - remote: buf.build + owner: grpc-ecosystem + repository: grpc-gateway + commit: 3f42134f4c564983838425bc43c7a65f + digest: shake256:3d11d4c0fe5e05fda0131afefbce233940e27f0c31c5d4e385686aea58ccd30f72053f61af432fa83f1fc11cda57f5f18ca3da26a29064f73c5a0d076bba8d92 \ No newline at end of file diff --git a/examples/buf.yaml b/examples/buf.yaml new file mode 100644 index 00000000..2dc8eb0e --- /dev/null +++ b/examples/buf.yaml @@ -0,0 +1,22 @@ +version: v1 +deps: + - buf.build/bufbuild/protovalidate + - buf.build/googleapis/googleapis + - buf.build/grpc-ecosystem/grpc-gateway +breaking: + use: + - FILE + - PACKAGE + - WIRE_JSON + - WIRE +lint: + allow_comment_ignores: true + use: + - DEFAULT + except: + - PACKAGE_VERSION_SUFFIX + ignore_only: + PACKAGE_VERSION_SUFFIX: + - google/api/annotations.proto + - google/api/http.proto + - google/protobuf/wrappers.proto diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100644 index 00000000..47bb4783 --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,178 @@ + + + 4.0.0 + + sdk-pom + io.opentdf.platform + 0.7.5 + + + io.opentdf.platform + examples + + examples + + http://www.example.com + + + UTF-8 + 11 + + + + + + org.junit + junit-bom + 5.11.0 + pom + import + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + io.opentdf.platform + sdk + ${project.version} + + + com.google.code.gson + gson + 2.11.0 + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-api + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + + + io.opentdf.platform.App + + ${project.version} + io.opentdf.platform.App + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + io.opentdf.platform.App + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + + generateSources + generate-sources + + + + + + + + + + + + + + + run + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.5.0 + + + add-source + generate-sources + + add-source + + + + target/generated-sources + + + + + + + + + diff --git a/examples/src/main/java/io/opentdf/platform/App.java b/examples/src/main/java/io/opentdf/platform/App.java new file mode 100644 index 00000000..22958126 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/App.java @@ -0,0 +1,7 @@ +package io.opentdf.platform; + +public class App { + public static void main(String[] args) { + System.out.println("opentdf examples"); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/CreateAttribute.java b/examples/src/main/java/io/opentdf/platform/CreateAttribute.java new file mode 100644 index 00000000..ba72189e --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/CreateAttribute.java @@ -0,0 +1,36 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.AttributeRuleTypeEnum; + +import io.opentdf.platform.policy.attributes.*; +import io.opentdf.platform.policy.Attribute; + +import java.util.Arrays; + +public class CreateAttribute { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + CreateAttributeRequest request = CreateAttributeRequest.newBuilder() + .setNamespaceId("877990d1-609b-42ab-a273-4253b8b321eb") + .setName("test") + .setRule(AttributeRuleTypeEnum.forNumber(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF_VALUE)) + .addAllValues(Arrays.asList("test1", "test2")).build(); + + CreateAttributeResponse resp = sdk.getServices().attributes().createAttribute(request).get(); + + Attribute attribute = resp.getAttribute(); + + } +} diff --git a/examples/src/main/java/io/opentdf/platform/CreateNamespace.java b/examples/src/main/java/io/opentdf/platform/CreateNamespace.java new file mode 100644 index 00000000..112cde4f --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/CreateNamespace.java @@ -0,0 +1,27 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.namespaces.*; + +public class CreateNamespace { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + CreateNamespaceRequest request = CreateNamespaceRequest.newBuilder().setName("mynamespace.com").build(); + + CreateNamespaceResponse resp = sdk.getServices().namespaces().createNamespace(request).get(); + + System.out.println(resp.getNamespace().getName()); + + } +} diff --git a/examples/src/main/java/io/opentdf/platform/CreateSubjectConditionSet.java b/examples/src/main/java/io/opentdf/platform/CreateSubjectConditionSet.java new file mode 100644 index 00000000..0eb6c5a2 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/CreateSubjectConditionSet.java @@ -0,0 +1,47 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.subjectmapping.*; +import io.opentdf.platform.policy.SubjectMapping; +import io.opentdf.platform.policy.SubjectConditionSet; +import io.opentdf.platform.policy.SubjectSet; +import io.opentdf.platform.policy.ConditionGroup; +import io.opentdf.platform.policy.Condition; +import io.opentdf.platform.policy.ConditionBooleanTypeEnum; +import io.opentdf.platform.policy.SubjectMappingOperatorEnum; + + +public class CreateSubjectConditionSet { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + var subjectset = SubjectSet.newBuilder().addConditionGroups(ConditionGroup.newBuilder() + .setBooleanOperator(ConditionBooleanTypeEnum.CONDITION_BOOLEAN_TYPE_ENUM_AND) + .addConditions(Condition.newBuilder() + .setSubjectExternalSelectorValue(".myfield") + .setOperator(SubjectMappingOperatorEnum.SUBJECT_MAPPING_OPERATOR_ENUM_IN) + .addSubjectExternalValues("myvalue") + )); + + CreateSubjectConditionSetRequest request = CreateSubjectConditionSetRequest.newBuilder() + .setSubjectConditionSet( + SubjectConditionSetCreate.newBuilder().addSubjectSets(subjectset)) + .build(); + + CreateSubjectConditionSetResponse resp = sdk.getServices().subjectMappings().createSubjectConditionSet(request).get(); + + SubjectConditionSet scs = resp.getSubjectConditionSet(); + + System.out.println(scs.getId()); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/CreateSubjectMapping.java b/examples/src/main/java/io/opentdf/platform/CreateSubjectMapping.java new file mode 100644 index 00000000..41f07336 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/CreateSubjectMapping.java @@ -0,0 +1,34 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.subjectmapping.*; +import io.opentdf.platform.policy.SubjectMapping; +import io.opentdf.platform.policy.Action; + +public class CreateSubjectMapping { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + CreateSubjectMappingRequest request = CreateSubjectMappingRequest.newBuilder() + .setAttributeValueId("33c47777-f3b6-492d-bcd2-5329e0aab642") + .addActions(Action.newBuilder().setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) + .setExistingSubjectConditionSetId("9009fde8-d22b-4dfb-a456-f9ce6943244a") + .build(); + + CreateSubjectMappingResponse resp = sdk.getServices().subjectMappings().createSubjectMapping(request).get(); + + SubjectMapping sm = resp.getSubjectMapping(); + + System.out.println(sm.getId()); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/DecryptExample.java b/examples/src/main/java/io/opentdf/platform/DecryptExample.java new file mode 100644 index 00000000..693192ca --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/DecryptExample.java @@ -0,0 +1,42 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; +import java.nio.file.StandardOpenOption; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.nimbusds.jose.JOSEException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import org.apache.commons.codec.DecoderException; + + +public class DecryptExample { + public static void main(String[] args) throws IOException, + InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC, + JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + Path path = Paths.get("my.ciphertext"); + try (var in = FileChannel.open(path, StandardOpenOption.READ)) { + var reader = new TDF().loadTDF(in, sdk.getServices().kas()); + reader.readPayload(System.out); + } + } +} diff --git a/examples/src/main/java/io/opentdf/platform/EncryptExample.java b/examples/src/main/java/io/opentdf/platform/EncryptExample.java new file mode 100644 index 00000000..6da5d47c --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/EncryptExample.java @@ -0,0 +1,39 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; +import java.io.ByteArrayInputStream; +import java.io.BufferedOutputStream; +import java.nio.charset.StandardCharsets; +import java.io.FileOutputStream; + +import com.nimbusds.jose.JOSEException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +public class EncryptExample { + public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException { + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + var kasInfo = new Config.KASInfo(); + kasInfo.URL = "http://localhost:8080/kas"; + + var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo), Config.withDataAttributes("https://example.com/attr/color/value/red")); + + String str = "Hello, World!"; + + // Convert String to InputStream + var in = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + FileOutputStream fos = new FileOutputStream("my.ciphertext"); + + new TDF().createTDF(in, fos, tdfConfig, + sdk.getServices().kas(), + sdk.getServices().attributes()); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/GetDecisions.java b/examples/src/main/java/io/opentdf/platform/GetDecisions.java new file mode 100644 index 00000000..4bac7694 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/GetDecisions.java @@ -0,0 +1,37 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.authorization.*; +import io.opentdf.platform.policy.Action; + +import java.util.List; + +public class GetDecisions { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + GetDecisionsRequest request = GetDecisionsRequest.newBuilder() + .addDecisionRequests(DecisionRequest.newBuilder() + .addEntityChains(EntityChain.newBuilder().setId("ec1").addEntities(Entity.newBuilder().setId("entity-1").setClientId("opentdf"))) + .addActions(Action.newBuilder().setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) + .addResourceAttributes(ResourceAttribute.newBuilder().setResourceAttributesId("resource-attribute-1") + .addAttributeValueFqns("https://mynamespace.com/attr/test/value/test1")) + ).build(); + + GetDecisionsResponse resp = sdk.getServices().authorization().getDecisions(request).get(); + + List decisions = resp.getDecisionResponsesList(); + + System.out.println(DecisionResponse.Decision.forNumber(decisions.get(0).getDecisionValue())); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/GetEntitlements.java b/examples/src/main/java/io/opentdf/platform/GetEntitlements.java new file mode 100644 index 00000000..ff484b80 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/GetEntitlements.java @@ -0,0 +1,32 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.authorization.*; + +import java.util.List; + +public class GetEntitlements { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() + .addEntities(Entity.newBuilder().setId("entity-1").setClientId("opentdf")) + .build(); + + GetEntitlementsResponse resp = sdk.getServices().authorization().getEntitlements(request).get(); + + List entitlements = resp.getEntitlementsList(); + + System.out.println(entitlements.get(0).getAttributeValueFqnsList()); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/ListAttributes.java b/examples/src/main/java/io/opentdf/platform/ListAttributes.java new file mode 100644 index 00000000..366af20a --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/ListAttributes.java @@ -0,0 +1,34 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.AttributeRuleTypeEnum; + +import io.opentdf.platform.policy.attributes.*; +import io.opentdf.platform.policy.Attribute; + +import java.util.List; + +public class ListAttributes { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + ListAttributesRequest request = ListAttributesRequest.newBuilder() + .setNamespace("mynamespace.com").build(); + + ListAttributesResponse resp = sdk.getServices().attributes().listAttributes(request).get(); + + List attributes = resp.getAttributesList(); + + System.out.println(resp.getAttributesCount()); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/ListNamespaces.java b/examples/src/main/java/io/opentdf/platform/ListNamespaces.java new file mode 100644 index 00000000..43e514a5 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/ListNamespaces.java @@ -0,0 +1,27 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.namespaces.*; +import io.opentdf.platform.policy.Namespace; + +public class ListNamespaces { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + ListNamespacesRequest request = ListNamespacesRequest.newBuilder().build(); + + ListNamespacesResponse resp = sdk.getServices().namespaces().listNamespaces(request).get(); + + java.util.List namespaces = resp.getNamespacesList(); + } +} diff --git a/examples/src/main/java/io/opentdf/platform/ListSubjectMappings.java b/examples/src/main/java/io/opentdf/platform/ListSubjectMappings.java new file mode 100644 index 00000000..12bc4ab7 --- /dev/null +++ b/examples/src/main/java/io/opentdf/platform/ListSubjectMappings.java @@ -0,0 +1,32 @@ +package io.opentdf.platform; +import io.opentdf.platform.sdk.*; + +import java.util.concurrent.ExecutionException; + +import io.opentdf.platform.policy.subjectmapping.*; +import io.opentdf.platform.policy.SubjectMapping; + +import java.util.List; + +public class ListSubjectMappings { + public static void main(String[] args) throws ExecutionException, InterruptedException{ + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) + .build(); + + ListSubjectMappingsRequest request = ListSubjectMappingsRequest.newBuilder().build(); + + ListSubjectMappingsResponse resp = sdk.getServices().subjectMappings().listSubjectMappings(request).get(); + + List sms = resp.getSubjectMappingsList(); + + System.out.println(sms.size()); + System.out.println(sms.get(0).getId()); + } +} diff --git a/examples/src/main/resources/log4j2.xml b/examples/src/main/resources/log4j2.xml new file mode 100644 index 00000000..da185a60 --- /dev/null +++ b/examples/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 3307b7e1..826a7dcc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.opentdf.platform sdk-pom - 0.7.4-SNAPSHOT + 0.7.5 io.opentdf.platform:sdk-pom OpenTDF Java SDK https://github.com/opentdf/java-sdk @@ -267,6 +267,7 @@ sdk cmdline + examples true @@ -342,5 +343,54 @@ + + coverage + + sdk + + + opentdf + opentdf_java-sdk + https://sonarcloud.io + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + report-aggregate + + + + report + test + + report + + + ${project.parent.basedir}/target/jacoco.exec + + XML + + + + + post-test-report + prepare-package + + report + + + + + + + \ No newline at end of file diff --git a/sdk/pom.xml b/sdk/pom.xml index 890ba1e3..678c78b0 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -6,7 +6,7 @@ sdk-pom io.opentdf.platform - 0.7.4-SNAPSHOT + 0.7.5 jar @@ -275,6 +275,36 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + ${project.parent.basedir}/target/jacoco.exec + + + + report + test + + report + + + ${project.parent.basedir}/target/jacoco.exec + ${project.parent.basedir}/target/site/jacoco/ + + XML + + + + + \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java index 0d79c460..71445f69 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java @@ -12,6 +12,11 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +/** + * The AesGcm class provides encryption and decryption methods using AES-GCM mode. + * It includes methods to encrypt and decrypt byte arrays using a specified + * symmetric key. + */ public class AesGcm { public static final int GCM_NONCE_LENGTH = 12; // in bytes public static final int GCM_TAG_LENGTH = 16; // in bytes diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 510a008c..c31fc1b0 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -3,6 +3,10 @@ import java.util.Objects; +/** + * Represents the configuration for assertions, encapsulating various types, scopes, states, keys, + * and statements involved in assertion handling. + */ public class AssertionConfig { public enum Type { @@ -30,6 +34,11 @@ public enum Scope { Scope(String scope) { this.scope = scope; } + + @Override + public String toString() { + return scope; + } } public enum AssertionKeyAlg { @@ -47,6 +56,11 @@ public enum AppliesToState { AppliesToState(String state) { this.state = state; } + + @Override + public String toString() { + return state; + } } public enum BindingMethod { @@ -57,6 +71,11 @@ public enum BindingMethod { BindingMethod(String method) { this.method = method; } + + @Override + public String toString() { + return method; + } } static public class AssertionKey { diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java b/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java index b7eb1e92..b76d1523 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java @@ -9,6 +9,9 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; +/** + * Class providing functionality for asymmetric decryption using an RSA private key. + */ public class AsymDecryption { private final PrivateKey privateKey; private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java b/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java index 415fc375..3a81b1f5 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java @@ -15,6 +15,10 @@ import java.util.Base64; import java.util.Objects; +/** + * AsymEncryption class provides methods for asymmetric encryption and + * handling public keys in PEM format. + */ public class AsymEncryption { private final PublicKey publicKey; private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----"; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AutoConfigureException.java b/sdk/src/main/java/io/opentdf/platform/sdk/AutoConfigureException.java index c134806c..157e6a8a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AutoConfigureException.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AutoConfigureException.java @@ -1,5 +1,8 @@ package io.opentdf.platform.sdk; +/** + * Exception thrown when automatic configuration fails. + */ public class AutoConfigureException extends RuntimeException { public AutoConfigureException(String message) { super(message); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 372daee2..6113a681 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -35,7 +35,11 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -// Attribute rule types: operators! +/** + * The RuleType class defines a set of constants that represent various types of attribute rules. + * These constants are used to specify the nature and behavior of attribute rules in the context + * of key management and policy enforcement. + */ class RuleType { public static final String HIERARCHY = "hierarchy"; public static final String ALL_OF = "allOf"; @@ -44,6 +48,12 @@ class RuleType { public static final String EMPTY_TERM = "DEFAULT"; } +/** + * The Autoconfigure class provides methods for configuring and retrieving + * grants related to attribute values and KAS (Key Access Server) keys. + * This class includes functionality to create granter instances based on + * attributes either from a list of attribute values or from a service. + */ public class Autoconfigure { public static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index a8766820..1f44854a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -10,6 +10,10 @@ import java.util.*; import java.util.function.Consumer; +/** + * Configuration class for setting various configurations related to TDF. + * Contains nested classes and enums for specific configuration settings. + */ public class Config { public static final int TDF3_KEY_SIZE = 2048; @@ -87,12 +91,14 @@ AssertionConfig.AssertionKey getKey(String key) { public static class TDFReaderConfig { // Optional Map of Assertion Verification Keys - AssertionVerificationKeys assertionVerificationKeys; + AssertionVerificationKeys assertionVerificationKeys = new AssertionVerificationKeys(); + boolean disableAssertionVerification; } @SafeVarargs public static TDFReaderConfig newTDFReaderConfig(Consumer... options) { TDFReaderConfig config = new TDFReaderConfig(); + config.disableAssertionVerification = false; for (Consumer option : options) { option.accept(config); } @@ -104,6 +110,10 @@ public static Consumer withAssertionVerificationKeys( return (TDFReaderConfig config) -> config.assertionVerificationKeys = assertionVerificationKeys; } + public static Consumer withDisableAssertionVerification(boolean disable) { + return (TDFReaderConfig config) -> config.disableAssertionVerification = disable; + } + public static class TDFConfig { public Boolean autoconfigure; public int defaultSegmentSize; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java index 6a53e7e5..404637df 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java @@ -5,6 +5,9 @@ import java.security.*; import java.util.Base64; +/** + * Utility class for cryptographic operations such as generating RSA key pairs and calculating HMAC. + */ public class CryptoUtils { private static final int KEYPAIR_SIZE = 2048; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java b/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java index e8ac4648..8bba58bd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java @@ -1,5 +1,10 @@ package io.opentdf.platform.sdk; +/** + * InvalidZipException is thrown to indicate that a ZIP file being read + * is invalid or corrupted in some way. This exception extends RuntimeException, + * allowing it to be thrown during the normal operation of the Java Virtual Machine. + */ public class InvalidZipException extends RuntimeException { public InvalidZipException(String message) { super(message); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index 4732865b..806d48d0 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -9,13 +9,17 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import io.grpc.Status; import io.opentdf.platform.kas.AccessServiceGrpc; import io.opentdf.platform.kas.PublicKeyRequest; import io.opentdf.platform.kas.PublicKeyResponse; import io.opentdf.platform.kas.RewrapRequest; +import io.opentdf.platform.kas.RewrapResponse; import io.opentdf.platform.sdk.Config.KASInfo; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; +import io.opentdf.platform.sdk.TDF.KasBadRequestException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -30,6 +34,11 @@ import static java.lang.String.format; +/** + * A client implementation that communicates with a Key Access Service (KAS). + * This class provides methods to retrieve public keys, unwrap encrypted keys, + * and manage key caches. + */ public class KASClient implements SDK.KAS { private final Function channelFactory; @@ -177,9 +186,19 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) { .newBuilder() .setSignedRequestToken(jwt.serialize()) .build(); - var response = getStub(keyAccess.url).rewrap(request); - var wrappedKey = response.getEntityWrappedKey().toByteArray(); - return decryptor.decrypt(wrappedKey); + RewrapResponse response; + try { + response = getStub(keyAccess.url).rewrap(request); + var wrappedKey = response.getEntityWrappedKey().toByteArray(); + return decryptor.decrypt(wrappedKey); + } catch (StatusRuntimeException e) { + if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) { + // 400 Bad Request + throw new KasBadRequestException("rewrap request 400: " + e.toString()); + } + throw e; + } + } public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java index 22c4764c..5879dd05 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java @@ -8,6 +8,10 @@ import java.util.HashMap; import java.util.Map; +/** + * Class representing a cache for KAS (Key Access Server) information. + * It stores key information along with a timestamp to manage the freshness of cached data. + */ public class KASKeyCache { private static final Logger log = LoggerFactory.getLogger(KASKeyCache.class); Map cache; @@ -50,6 +54,17 @@ public void store(Config.KASInfo kasInfo) { } } +/** + * A class representing Key Aggregation Service (KAS) information along with a timestamp. + *

+ * This class holds information related to KAS, as well as a timestamp indicating when the + * information was recorded or generated. It encapsulates two main attributes: {@code kasInfo} + * and {@code timestamp}. + *

+ * The {@code kasInfo} field is an instance of {@code Config.KASInfo}, which contains the KAS-specific + * data. The {@code timestamp} field is an instance of {@code LocalDateTime}, representing + * the date and time when the KAS information was recorded. + */ class TimeStampedKASInfo { Config.KASInfo kasInfo; LocalDateTime timestamp; @@ -60,6 +75,13 @@ public TimeStampedKASInfo(Config.KASInfo kasInfo, LocalDateTime timestamp) { } } +/** + * Represents a request for a Key Access Server (KAS) key. + * This class is used to request keys using a specified URL and algorithm. + * + * This class also overrides equals and hashCode methods + * to ensure proper functioning within hash-based collections. + */ class KASKeyRequest { private String url; private String algorithm; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java index f9434c6b..aca6d1cd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -10,6 +10,9 @@ import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; + +import io.opentdf.platform.sdk.TDF.AssertionException; + import org.apache.commons.codec.binary.Hex; import org.erdtman.jcs.JsonCanonicalizer; @@ -27,6 +30,10 @@ import java.util.List; import java.util.Objects; +/** + * The Manifest class represents a detailed structure encapsulating various aspects + * of data integrity, encryption, payload, and assertions within a certain context. + */ public class Manifest { private static final String kAssertionHash = "assertionHash"; @@ -377,7 +384,7 @@ public void sign(final HashValues hashValues, final AssertionConfig.AssertionKey public Assertion.HashValues verify(AssertionConfig.AssertionKey assertionKey) throws ParseException, JOSEException { if (binding == null) { - throw new SDKException("Binding is null in assertion"); + throw new AssertionException("Binding is null in assertion", this.id); } String signatureString = binding.signature; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java index 9132e570..5695713a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java @@ -16,6 +16,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * The NanoTDF class provides methods to create and read NanoTDF (Tiny Data Format) files. + * The NanoTDF format is intended for securely encrypting small data payloads using elliptic-curve cryptography + * and authenticated encryption. + */ public class NanoTDF { public static Logger logger = LoggerFactory.getLogger(NanoTDF.class); @@ -83,7 +88,6 @@ public int createNanoTDF(ByteBuffer data, OutputStream outputStream, MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hashOfSalt = digest.digest(MAGIC_NUMBER_AND_VERSION); byte[] key = ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey); - logger.debug("createNanoTDF key is - {}", Base64.getEncoder().encodeToString(key)); // Encrypt policy PolicyObject policyObject = createPolicyObject(nanoTDFConfig.attributes); @@ -130,11 +134,13 @@ public int createNanoTDF(ByteBuffer data, OutputStream outputStream, // Encrypt the data byte[] actualIV = new byte[kIvPadding + kNanoTDFIvSize]; - byte[] iv = new byte[kNanoTDFIvSize]; - SecureRandom.getInstanceStrong().nextBytes(iv); - System.arraycopy(iv, 0, actualIV, kIvPadding, iv.length); + do { + byte[] iv = new byte[kNanoTDFIvSize]; + SecureRandom.getInstanceStrong().nextBytes(iv); + System.arraycopy(iv, 0, actualIV, kIvPadding, iv.length); + } while (Arrays.equals(actualIV, kEmptyIV)); // if match, we need to retry to prevent key + iv reuse with the policy - byte[] cipherData = gcm.encrypt(actualIV, authTagSize, data.array(), 0, dataSize); + byte[] cipherData = gcm.encrypt(actualIV, authTagSize, data.array(), data.arrayOffset(), dataSize); // Write the length of the payload as int24 int cipherDataLengthWithoutPadding = cipherData.length - kIvPadding; @@ -168,7 +174,6 @@ public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, byte[] key = kas.unwrapNanoTDF(header.getECCMode().getEllipticCurveType(), base64HeaderData, kasUrl); - logger.debug("readNanoTDF key is {}", Base64.getEncoder().encodeToString(key)); byte[] payloadLengthBuf = new byte[4]; nanoTDF.get(payloadLengthBuf, 1, 3); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java b/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java index 15d2d0bb..9d708699 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java @@ -2,6 +2,9 @@ import java.util.List; +/** + * The PolicyObject class represents a policy with a unique identifier and a body containing data attributes. + */ public class PolicyObject { static public class AttributeObject { public String attribute; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java index fe2a8005..14affa4c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -34,11 +34,20 @@ public class SDK implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(SDK.class); + /** + * Closes the SDK, including its associated services. + * + * @throws Exception if an error occurs while closing the services. + */ @Override public void close() throws Exception { services.close(); } + /** + * KAS (Key Access Service) interface to define methods related to key access and management. + * This interface extends AutoCloseable to allow for resource management during close operations. + */ public interface KAS extends AutoCloseable { Config.KASInfo getPublicKey(Config.KASInfo kasInfo); @@ -51,7 +60,10 @@ public interface KAS extends AutoCloseable { KASKeyCache getKeyCache(); } - // TODO: add KAS + /** + * The Services interface provides access to various gRPC service stubs and a Key Authority Service (KAS). + * It extends the AutoCloseable interface, allowing for the release of resources when no longer needed. + */ public interface Services extends AutoCloseable { AuthorizationServiceFutureStub authorization(); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java index 4068b5e0..8c6b1804 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java @@ -69,7 +69,6 @@ public SDKBuilder sslFactory(SSLFactory sslFactory) { * Add SSL Context with trusted certs from certDirPath * * @param certsDirPath Path to a directory containing .pem or .crt trusted certs - * @return */ public SDKBuilder sslFactoryFromDirectory(String certsDirPath) throws Exception { File certsDir = new File(certsDirPath); @@ -91,7 +90,6 @@ public SDKBuilder sslFactoryFromDirectory(String certsDirPath) throws Exception * * @param keystorePath Path to keystore * @param keystorePassword Password to keystore - * @return */ public SDKBuilder sslFactoryFromKeyStore(String keystorePath, String keystorePassword) { this.sslFactory = SSLFactory.builder().withDefaultTrustMaterial().withSystemTrustMaterial() diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDKException.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKException.java index 2d0b390d..58e7adbe 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKException.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKException.java @@ -1,5 +1,10 @@ package io.opentdf.platform.sdk; +/** + * SDKException serves as a custom exception class for handling errors + * specific to the SDK's operations and processes. It extends + * RuntimeException, making it an unchecked exception. + */ public class SDKException extends RuntimeException { public SDKException(String message, Exception reason) { super(message, reason); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index ba7c5b97..4c6eb6da 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -28,10 +28,23 @@ import java.util.*; import java.util.concurrent.ExecutionException; +/** + * The TDF class is responsible for handling operations related to + * Trusted Data Format (TDF). It includes methods to create and load + * TDF objects, as well as utility functions to handle cryptographic + * operations and configurations. + */ public class TDF { private final long maximumSize; + /** + * Constructs a new TDF instance using the default maximum input size defined by MAX_TDF_INPUT_SIZE. + *

+ * This constructor is primarily used to initialize the TDF object with the standard maximum + * input size, which controls the maximum size of the input data that can be processed. + * For test purposes, an alternative constructor allows for setting a custom maximum input size. + */ public TDF() { this(MAX_TDF_INPUT_SIZE); } @@ -106,30 +119,48 @@ public FailedToCreateGMAC(String errorMessage) { } } - public static class NotValidateRootSignature extends RuntimeException { - public NotValidateRootSignature(String errorMessage) { + public static class TDFReadFailed extends RuntimeException { + public TDFReadFailed(String errorMessage) { + super(errorMessage); + } + } + + public static class TamperException extends SDKException { + public TamperException(String errorMessage) { + super("[tamper detected] "+errorMessage); + } + } + + public static class RootSignatureValidationException extends TamperException { + public RootSignatureValidationException(String errorMessage) { super(errorMessage); } } - public static class SegmentSizeMismatch extends RuntimeException { + public static class SegmentSizeMismatch extends TamperException { public SegmentSizeMismatch(String errorMessage) { super(errorMessage); } } - public static class SegmentSignatureMismatch extends RuntimeException { + public static class SegmentSignatureMismatch extends TamperException { public SegmentSignatureMismatch(String errorMessage) { super(errorMessage); } } - public static class TDFReadFailed extends RuntimeException { - public TDFReadFailed(String errorMessage) { + public static class KasBadRequestException extends TamperException { + public KasBadRequestException(String errorMessage) { super(errorMessage); } } + public static class AssertionException extends TamperException { + public AssertionException(String errorMessage, String id) { + super("assertion id: "+ id + "; " + errorMessage); + } + } + public static class EncryptedMetadata { private String ciphertext; private String iv; @@ -537,9 +568,15 @@ public List defaultKases(TDFConfig config) { return defk; } + public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas) + throws DecoderException, IOException, ParseException, NoSuchAlgorithmException, JOSEException { + return loadTDF(tdf, kas, new Config.TDFReaderConfig()); + } + + public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, - Config.AssertionVerificationKeys... assertionVerificationKeys) - throws NotValidateRootSignature, SegmentSizeMismatch, + Config.TDFReaderConfig tdfReaderConfig) + throws RootSignatureValidationException, SegmentSizeMismatch, IOException, FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { TDFReader tdfReader = new TDFReader(tdf); @@ -647,7 +684,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, } if (rootSignature.compareTo(rootSigValue) != 0) { - throw new NotValidateRootSignature("root signature validation failed"); + throw new RootSignatureValidationException("root signature validation failed"); } int segmentSize = manifest.encryptionInformation.integrityInformation.segmentSizeDefault; @@ -659,10 +696,16 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, // Validate assertions for (var assertion : manifest.assertions) { + // Skip assertion verification if disabled + if (tdfReaderConfig.disableAssertionVerification) { + break; + } + // Set default to HS256 var assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey); - if (assertionVerificationKeys != null && assertionVerificationKeys.length > 0) { - var keyForAssertion = assertionVerificationKeys[0].getKey(assertion.id); + Config.AssertionVerificationKeys assertionVerificationKeys = tdfReaderConfig.assertionVerificationKeys; + if (!assertionVerificationKeys.isEmpty()) { + var keyForAssertion = assertionVerificationKeys.getKey(assertion.id); if (keyForAssertion != null) { assertionKey = keyForAssertion; } @@ -676,11 +719,11 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, var encodeSignature = Base64.getEncoder().encodeToString(signature.getBytes()); if (!Objects.equals(hashOfAssertion, hashValues.getAssertionHash())) { - throw new SDKException("assertion hash mismatch"); + throw new AssertionException("assertion hash mismatch", assertion.id); } if (!Objects.equals(encodeSignature, hashValues.getSignature())) { - throw new SDKException("failed integrity check on assertion signature"); + throw new AssertionException("failed integrity check on assertion signature", assertion.id); } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java index 24b05940..6691888a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java @@ -12,6 +12,11 @@ import static io.opentdf.platform.sdk.TDFWriter.TDF_MANIFEST_FILE_NAME; import static io.opentdf.platform.sdk.TDFWriter.TDF_PAYLOAD_FILE_NAME; +/** + * TDFReader is responsible for reading and processing Trusted Data Format (TDF) files. + * The class initializes with a TDF file channel, extracts the manifest and payload entries, + * and provides methods to retrieve the manifest content, read payload bytes, and read policy objects. + */ public class TDFReader { private final ZipReader.Entry manifestEntry; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java index a5f3509b..048822f6 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java @@ -4,6 +4,10 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; +/** + * The TDFWriter class provides functionalities for creating a TDF (Trusted Data Format) archive. + * This includes appending a manifest file and appending payload data to the archive. + */ public class TDFWriter { public static final String TDF_PAYLOAD_FILE_NAME = "0.payload"; public static final String TDF_MANIFEST_FILE_NAME = "0.manifest.json"; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java b/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java index 17bc51c5..cb6d5ffd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java @@ -12,6 +12,12 @@ import java.util.ArrayList; import java.util.List; +/** + * The ZipReader class provides functionality to read basic ZIP file + * structures, such as the End of Central Directory Record and the + * Local File Header. This class supports standard ZIP archives as well + * as ZIP64 format. + */ public class ZipReader { public static final Logger logger = LoggerFactory.getLogger(ZipReader.class); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java b/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java index d775a0a8..71aea34c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java @@ -9,6 +9,10 @@ import java.util.ArrayList; import java.util.zip.CRC32; +/** + * The ZipWriter class provides functionalities to create ZIP archive files. + * It writes files and data to an underlying output stream in the ZIP file format. + */ public class ZipWriter { private static final int ZIP_VERSION = 0x2D; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/package-info.java b/sdk/src/main/java/io/opentdf/platform/sdk/package-info.java new file mode 100644 index 00000000..cd3ae44a --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/package-info.java @@ -0,0 +1,19 @@ +/** + * The io.opentdf.platform.sdk package provides a comprehensive set of + * classes, interfaces, enums, and exceptions designed for interacting with + * the OpenTDF platform. At its core, the {@link io.opentdf.platform.sdk.SDK} class + * serves as the centerpiece for this package, representing a software development kit + * that facilitates integration with the OpenTDF services. The package also includes + * classes for various encryption and decryption operations such as {@link io.opentdf.platform.sdk.AesGcm} + * and its inner classes, as well as support for assertions and configurations through + * classes like {@link io.opentdf.platform.sdk.AssertionConfig} and {@link io.opentdf.platform.sdk.Config}. + * Additionally, utility classes such as {@link io.opentdf.platform.sdk.CryptoUtils} and + * functionality for reading and writing TDF files are provided via {@link io.opentdf.platform.sdk.TDFReader} + * and {@link io.opentdf.platform.sdk.TDFWriter}. The package also defines a collection of + * enums for various configurations and state definitions, alongside a comprehensive + * set of exceptions to handle different error scenarios, ensuring robust and secure + * operations within the OpenTDF ecosystem. + * + * @since 1.0 + */ +package io.opentdf.platform.sdk; \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java index fb44663c..11a41bc8 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java @@ -38,7 +38,7 @@ public void createAndDecryptTdfIT() throws Exception { tdf.createTDF(plainTextInputStream, tdfOutputStream, config, sdk.kas(), sdk.attributes()); var unwrappedData = new java.io.ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.kas()); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.kas()); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)).isEqualTo("text"); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index e4fed930..acd1fd15 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -2,10 +2,12 @@ import com.google.common.util.concurrent.ListenableFuture; +import com.nimbusds.jose.JOSEException; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; import io.opentdf.platform.policy.attributes.AttributesServiceGrpc; import io.opentdf.platform.sdk.Config.KASInfo; +import io.opentdf.platform.sdk.TDF.Reader; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import org.junit.jupiter.api.BeforeAll; @@ -29,7 +31,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; - import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -136,8 +137,10 @@ void testSimpleTDFEncryptAndDecrypt() throws Exception { key); var unwrappedData = new ByteArrayOutputStream(); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - assertionVerificationKeys); + readerConfig); assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); reader.readPayload(unwrappedData); @@ -192,8 +195,10 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); var unwrappedData = new ByteArrayOutputStream(); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - assertionVerificationKeys); + readerConfig); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -201,6 +206,61 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { .isEqualTo(plainText); } + @Test + void testWithAssertionVerificationDisabled() throws Exception { + + ListenableFuture resp1 = mock(ListenableFuture.class); + lenient().when(resp1.get()).thenReturn(GetAttributeValuesByFqnsResponse.newBuilder().build()); + lenient().when(attributeGrpcStub.getAttributeValuesByFqns(any(GetAttributeValuesByFqnsRequest.class))) + .thenReturn(resp1); + + String assertion1Id = "assertion1"; + var keypair = CryptoUtils.generateRSAKeypair(); + var assertionConfig = new AssertionConfig(); + assertionConfig.id = assertion1Id; + assertionConfig.type = AssertionConfig.Type.BaseAssertion; + assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig.statement = new AssertionConfig.Statement(); + assertionConfig.statement.format = "base64binary"; + assertionConfig.statement.schema = "text"; + assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPrivate()); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getKASInfos()), + Config.withAssertionConfig(assertionConfig)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, attributeGrpcStub); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.keys.put(assertion1Id, + new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); + + var unwrappedData = new ByteArrayOutputStream(); + assertThrows(JOSEException.class, () -> { + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, + new Config.TDFReaderConfig()); + }); + + // try with assertion verification disabled and not passing the assertion verification keys + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withDisableAssertionVerification(true)); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, + readerConfig); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + } @Test void testSimpleTDFWithAssertionWithHS256() throws Exception { @@ -244,7 +304,8 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, attributeGrpcStub); var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + kas, new Config.TDFReaderConfig()); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -272,6 +333,62 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { } } + @Test + void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { + + ListenableFuture resp1 = mock(ListenableFuture.class); + lenient().when(resp1.get()).thenReturn(GetAttributeValuesByFqnsResponse.newBuilder().build()); + lenient().when(attributeGrpcStub.getAttributeValuesByFqns(any(GetAttributeValuesByFqnsRequest.class))) + .thenReturn(resp1); + + // var keypair = CryptoUtils.generateRSAKeypair(); + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + String assertion1Id = "assertion1"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = assertion1Id; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig1.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getKASInfos()), + Config.withAssertionConfig(assertionConfig1)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, attributeGrpcStub); + + byte[] notkey = new byte[32]; + secureRandom.nextBytes(notkey); + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, + notkey); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); + + var unwrappedData = new ByteArrayOutputStream(); + Reader reader; + try { + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig); + throw new RuntimeException("assertion verify key error thrown"); + + } catch (SDKException e) { + assertThat(e).hasMessageContaining("verify"); + } + } + @Test public void testCreatingTDFWithMultipleSegments() throws Exception {