From 63d003d3adaee8876620fd347ada6ffec9052360 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Tue, 27 Jan 2026 14:42:52 -0800 Subject: [PATCH 1/9] Fix potential null pointer exceptions --- .github/workflows/ci.yml | 5 ++- .../kms/v1/BatchDocumentKeys.java | 7 ++++ .../tenantsecurity/kms/v1/DerivedKey.java | 2 +- .../kms/v1/DeterministicCryptoUtils.java | 2 +- .../kms/v1/TenantSecurityClient.java | 2 +- .../kms/v1/UnwrappedDocumentKey.java | 2 +- .../kms/v1/WrappedDocumentKey.java | 2 +- .../tenantsecurity/kms/v1/LocalBatch.java | 2 +- .../kms/v1/LocalDeterministic.java | 4 +-- .../tenantsecurity/kms/v1/LocalRoundTrip.java | 1 - .../kms/v1/NotPrimaryAndDisabledConfigs.java | 1 - .../tenantsecurity/kms/v1/RekeyRoundTrip.java | 32 ++++++++----------- test-suites/allTest.sh | 4 +++ test-suites/test-all.xml | 18 +++++++++++ 14 files changed, 55 insertions(+), 29 deletions(-) create mode 100755 test-suites/allTest.sh create mode 100644 test-suites/test-all.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 818c5cb..e3146c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,10 @@ jobs: gcloud-auth: ${{ secrets.GCLOUD_AUTH }} env-file-path: .env.integration - name: integration test - run: env $(cat .env.integration) ./test-suites/integrationTest.sh + run: env $(cat .env.integration) ./test-suites/allTest.sh + env: + TENANT_ID: INTEGRATION-TEST-GCP + NEW_TENANT_ID: INTEGRATION-TEST-AWS build_examples: runs-on: ubuntu-22.04 diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java index 9e6bde6..cbec8c5 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java @@ -1,5 +1,6 @@ package com.ironcorelabs.tenantsecurity.kms.v1; +import java.util.Collections; import java.util.Map; import com.google.api.client.util.Key; @@ -16,10 +17,16 @@ public class BatchDocumentKeys { private Map failures; public Map getKeys() { + if (this.keys == null) { + return Collections.emptyMap(); + } return this.keys; } public Map getFailures() { + if (this.failures == null) { + return Collections.emptyMap(); + } return this.failures; } } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java index 73b2bd6..dac7692 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java @@ -24,7 +24,7 @@ public DerivedKey() {} byte[] getDerivedKeyBytes() { try { return Base64.getDecoder().decode(this.derivedKey); - } catch (IllegalArgumentException e) { + } catch (Exception e) { throw new IllegalArgumentException( "Derive keys response from the Tenant Security Proxy was not valid base64."); } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeterministicCryptoUtils.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeterministicCryptoUtils.java index dbee8cb..6715ea3 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeterministicCryptoUtils.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeterministicCryptoUtils.java @@ -106,7 +106,7 @@ static CompletableFuture decryptField( return key; }).thenCompose(key -> decryptBytes(parts.getEncryptedBytes(), key.getDerivedKeyBytes()))) .thenApply(decrypted -> new DeterministicPlaintextField(decrypted, - encryptedField.getDerivationPath(), encryptedField.getSecretPath())); + encryptedField.getSecretPath(), encryptedField.getDerivationPath())); } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityClient.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityClient.java index 0074447..8bf9d28 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityClient.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityClient.java @@ -189,7 +189,7 @@ public Builder allowInsecureHttp(boolean allow) { } /** - * Construct the TenantSecurityClient fron the builder. + * Construct the TenantSecurityClient from the builder. * * @return The newly constructed TenantSecurityClient. * @throws Exception If the tsp url isn't valid or if HTTPS is required and not provided. diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java index a2c58b7..cae6c40 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java @@ -14,7 +14,7 @@ public class UnwrappedDocumentKey { public byte[] getDekBytes() { try { return Base64.getDecoder().decode(this.dek); - } catch (IllegalArgumentException e) { + } catch (Exception e) { throw new IllegalArgumentException( "Unwrap DEK response from the Tenant Security Proxy was not valid base64."); } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java index 63f0250..1a25389 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java @@ -17,7 +17,7 @@ public class WrappedDocumentKey { public byte[] getDekBytes() { try { return Base64.getDecoder().decode(this.dek); - } catch (IllegalArgumentException e) { + } catch (Exception e) { throw new IllegalArgumentException( "Wrapped document key response from the Tenant Security Proxy was not valid base64."); } diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalBatch.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalBatch.java index c2185a5..7621312 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalBatch.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalBatch.java @@ -60,7 +60,7 @@ public void batchRoundtrip() throws Exception { TenantSecurityClient client = new TenantSecurityClient.Builder(TestSettings.TSP_ADDRESS + TestSettings.TSP_PORT, - this.API_KEY).build(); + this.API_KEY).allowInsecureHttp(true).build(); int batchSize = 25; int batchRepetitions = 50; diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalDeterministic.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalDeterministic.java index f158131..4cc6b01 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalDeterministic.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalDeterministic.java @@ -22,8 +22,8 @@ private void assertEqualBytes(byte[] one, byte[] two) throws Exception { } private DeterministicPlaintextField getRoundtripDataToEncrypt() { - return new DeterministicPlaintextField("Encrypt these bytes!".getBytes(), "deriv_path", - "secret_path"); + return new DeterministicPlaintextField("Encrypt these bytes!".getBytes(), "secret_path", + "deriv_path"); } private CompletableFuture createClient() { diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalRoundTrip.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalRoundTrip.java index 2930ee9..4d8ed4c 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalRoundTrip.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/LocalRoundTrip.java @@ -12,7 +12,6 @@ import com.ironcorelabs.tenantsecurity.kms.v1.exception.TenantSecurityException; import com.ironcorelabs.tenantsecurity.logdriver.v1.EventMetadata; import com.ironcorelabs.tenantsecurity.logdriver.v1.UserEvent; -import com.ironcorelabs.tenantsecurity.utils.CompletableFutures; import org.testng.annotations.Test; @Test(groups = {"local-integration"}) diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/NotPrimaryAndDisabledConfigs.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/NotPrimaryAndDisabledConfigs.java index 7ac8037..3ba5214 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/NotPrimaryAndDisabledConfigs.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/NotPrimaryAndDisabledConfigs.java @@ -9,7 +9,6 @@ import java.util.concurrent.ExecutionException; import com.ironcorelabs.tenantsecurity.TestUtils; import com.ironcorelabs.tenantsecurity.kms.v1.exception.TenantSecurityException; -import com.ironcorelabs.tenantsecurity.utils.CompletableFutures; import org.testng.annotations.Test; @Test(groups = {"dev-integration"}) diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyRoundTrip.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyRoundTrip.java index 89c7e28..c801c9a 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyRoundTrip.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyRoundTrip.java @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import com.ironcorelabs.tenantsecurity.kms.v1.exception.TenantSecurityException; import org.testng.annotations.Test; @@ -50,24 +49,21 @@ public void roundtripTest() throws Exception { new DocumentMetadata(tenant_id, "integrationTest", "sample", customFields, "customRayID"); Map documentMap = getRoundtripDataToEncrypt(); - CompletableFuture roundtrip = - TenantSecurityClient.create(tsp_address + tsp_port, api_key).thenCompose(client -> { - try { - return client.encrypt(documentMap, metadata).thenCompose(encryptedDocument -> { - return client.rekeyEdek(encryptedDocument.getEdek(), metadata, new_tenant_id) - .thenCompose(rekeyedEdek -> { - DocumentMetadata newMetadata = new DocumentMetadata(new_tenant_id, - "integrationTest", "sample", customFields, "customRayID"); - EncryptedDocument newDocument = - new EncryptedDocument(encryptedDocument.getEncryptedFields(), rekeyedEdek); - return client.decrypt(newDocument, newMetadata); - }); - }); - } catch (Exception e) { - throw new CompletionException(e); - } - }); try { + TenantSecurityClient client = + new TenantSecurityClient.Builder(tsp_address + tsp_port, api_key).allowInsecureHttp(true) + .build(); + CompletableFuture roundtrip = + client.encrypt(documentMap, metadata).thenCompose(encryptedDocument -> { + return client.rekeyEdek(encryptedDocument.getEdek(), metadata, new_tenant_id) + .thenCompose(rekeyedEdek -> { + DocumentMetadata newMetadata = new DocumentMetadata(new_tenant_id, + "integrationTest", "sample", customFields, "customRayID"); + EncryptedDocument newDocument = + new EncryptedDocument(encryptedDocument.getEncryptedFields(), rekeyedEdek); + return client.decrypt(newDocument, newMetadata); + }); + }); Map decryptedValuesMap = roundtrip.get().getDecryptedFields(); assertEqualBytes(decryptedValuesMap.get("doc1"), documentMap.get("doc1")); assertEqualBytes(decryptedValuesMap.get("doc2"), documentMap.get("doc2")); diff --git a/test-suites/allTest.sh b/test-suites/allTest.sh new file mode 100755 index 0000000..9d01814 --- /dev/null +++ b/test-suites/allTest.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Be sure to set API_KEY, TENANT_ID, and NEW_TENANT_ID env vars +cd "${0%/*/*}" # set the current directory to the one above this script +mvn -Dsuite=test-suites/test-all test diff --git a/test-suites/test-all.xml b/test-suites/test-all.xml new file mode 100644 index 0000000..32640a2 --- /dev/null +++ b/test-suites/test-all.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5877ccd4c525f33ffc1225a33ec101ea513cc98f Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Thu, 29 Jan 2026 13:41:50 -0800 Subject: [PATCH 2/9] Add NullParsingValidator interface instead --- .../kms/v1/BatchDocumentKeys.java | 17 +++--- .../kms/v1/DeriveKeyResponse.java | 8 ++- .../kms/v1/NullParsingValidator.java | 5 ++ .../kms/v1/RekeyedDocumentKey.java | 9 +++- .../kms/v1/TenantSecurityRequest.java | 15 ++++-- .../kms/v1/UnwrappedDocumentKey.java | 9 +++- .../kms/v1/VoidSecurityEventResponse.java | 6 +++ .../kms/v1/WrappedDocumentKey.java | 9 +++- .../kms/v1/JsonParsingTest.java | 52 +++++++++++++++++++ 9 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java create mode 100644 src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java create mode 100644 src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java index cbec8c5..c1bf281 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java @@ -1,15 +1,13 @@ package com.ironcorelabs.tenantsecurity.kms.v1; -import java.util.Collections; import java.util.Map; - import com.google.api.client.util.Key; /** * A map from a document ID to a either the wrapped or unwrapped version of a documents keys. Also * includes a map of failures if any problems occurred when performing the batch wrap operation. */ -public class BatchDocumentKeys { +public class BatchDocumentKeys implements NullParsingValidator { @Key private Map keys; @@ -17,16 +15,17 @@ public class BatchDocumentKeys { private Map failures; public Map getKeys() { - if (this.keys == null) { - return Collections.emptyMap(); - } return this.keys; } public Map getFailures() { - if (this.failures == null) { - return Collections.emptyMap(); - } return this.failures; } + + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException { + if (keys == null || failures == null) + throw new IllegalArgumentException( + "Batch response from the Tenant Security Proxy was not valid."); + } } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java index 5a96573..8e944f0 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java @@ -5,7 +5,7 @@ import com.google.api.client.util.Key; import com.ironcorelabs.tenantsecurity.kms.v1.exception.TspServiceException; -public final class DeriveKeyResponse { +public final class DeriveKeyResponse implements NullParsingValidator { @Key private boolean hasPrimaryConfig; @Key @@ -37,4 +37,10 @@ CompletableFuture getDerivedKeys(String secretPath, String derivat } return CompletableFuture.completedFuture(derivedKeys); } + + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException { + if (derivedKeys == null) + throw new IllegalArgumentException("TSP failed to derive keys."); + } } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java new file mode 100644 index 0000000..14321b4 --- /dev/null +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java @@ -0,0 +1,5 @@ +package com.ironcorelabs.tenantsecurity.kms.v1; + +interface NullParsingValidator { + void ensureNoNullsOrThrow() throws IllegalArgumentException; +} diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java index 1cdfae7..a7c7fb6 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java @@ -5,11 +5,18 @@ /** * An EDEK made by wrapping an existing encrypted document with a tenant's KMS, in Base64 format. */ -public class RekeyedDocumentKey { +public class RekeyedDocumentKey implements NullParsingValidator { @Key private String edek; public String getEdek() { return this.edek; } + + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException { + if (edek == null) + throw new IllegalArgumentException( + "Rekeyed document key response from the Tenant Security Proxy was not valid base64."); + } } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java index e30c5c3..e3ea798 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java @@ -153,13 +153,15 @@ enum SecretType { * Generic method for making a request to the provided URL with the provided post data. Returns an * instance of the provided generic JSON class or an error message with the provided error. */ - private CompletableFuture makeRequestAndParseFailure(GenericUrl url, - Map postData, Class jsonType, String errorMessage) { + private CompletableFuture makeRequestAndParseFailure( + GenericUrl url, Map postData, Class jsonType, String errorMessage) { return CompletableFuture.supplyAsync(() -> { try { HttpResponse resp = this.getApiRequest(postData, url).execute(); if (resp.isSuccessStatusCode()) { - return resp.parseAs(jsonType); + T parsed = resp.parseAs(jsonType); + parsed.ensureNoNullsOrThrow(); + return parsed; } throw parseFailureFromRequest(resp); } catch (Exception cause) { @@ -261,7 +263,12 @@ CompletableFuture logSecurityEvent(SecurityEvent event, EventMetadata meta String error = String.format( "Unable to make request to Tenant Security Proxy security event endpoint. Endpoint requested: %s", this.securityEventEndpoint); - return this.makeRequestAndParseFailure(this.securityEventEndpoint, postData, Void.class, error); + return this + .makeRequestAndParseFailure(this.securityEventEndpoint, postData, + VoidSecurityEventResponse.class, error) + // returns a Void + .thenRun(() -> { + }); } /** diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java index cae6c40..ea79c47 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java @@ -7,7 +7,7 @@ /** * Represents the JSON response object from the document/unwrap endpoint which includes the dek. */ -public class UnwrappedDocumentKey { +public class UnwrappedDocumentKey implements NullParsingValidator { @Key private String dek; @@ -19,4 +19,11 @@ public byte[] getDekBytes() { "Unwrap DEK response from the Tenant Security Proxy was not valid base64."); } } + + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException { + if (dek == null) + throw new IllegalArgumentException( + "Unwrap DEK response from the Tenant Security Proxy was not valid base64."); + } } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java new file mode 100644 index 0000000..1cf18fa --- /dev/null +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java @@ -0,0 +1,6 @@ +package com.ironcorelabs.tenantsecurity.kms.v1; + +public class VoidSecurityEventResponse implements NullParsingValidator { + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException {} +} diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java index 1a25389..0889f75 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java @@ -7,7 +7,7 @@ /** * A new DEK wrapped by the tenant's KMS and its encrypted form (EDEK), both in Base64 format. */ -public class WrappedDocumentKey { +public class WrappedDocumentKey implements NullParsingValidator { @Key private String dek; @@ -26,4 +26,11 @@ public byte[] getDekBytes() { public String getEdek() { return this.edek; } + + @Override + public void ensureNoNullsOrThrow() throws IllegalArgumentException { + if (edek == null || dek == null) + throw new IllegalArgumentException( + "Wrapped document key response from the Tenant Security Proxy was not valid base64."); + } } diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java new file mode 100644 index 0000000..39e9fdd --- /dev/null +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java @@ -0,0 +1,52 @@ +package com.ironcorelabs.tenantsecurity.kms.v1; + +import java.io.StringReader; +import org.testng.annotations.Test; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; + +@Test(groups = {"unit"}) +public class JsonParsingTest { + static JsonObjectParser parser = new JsonObjectParser(new GsonFactory()); + + @Test(expectedExceptions = IllegalArgumentException.class) + void batchKeysAreEmpty() throws Exception { + String json = "{}"; + BatchWrappedDocumentKeys type = parser.parseAndClose( + new StringReader(json), BatchWrappedDocumentKeys.class); + type.ensureNoNullsOrThrow(); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + void derivedKeyResponseIsEmpty() throws Exception { + String json = "{}"; + DeriveKeyResponse type = + parser.parseAndClose(new StringReader(json), DeriveKeyResponse.class); + type.ensureNoNullsOrThrow(); + + } + + @Test(expectedExceptions = IllegalArgumentException.class) + void unwrappedDocumentKeyErrors() throws Exception { + String json = "{}"; + UnwrappedDocumentKey type = parser.parseAndClose(new StringReader(json), + UnwrappedDocumentKey.class); + type.ensureNoNullsOrThrow(); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + void wrappedDocumentKeyErrors() throws Exception { + String json = "{}"; + WrappedDocumentKey type = + parser.parseAndClose(new StringReader(json), WrappedDocumentKey.class); + type.ensureNoNullsOrThrow(); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + void rekeyedDocumentKeyIsNull() throws Exception { + String json = "{}"; + RekeyedDocumentKey type = + parser.parseAndClose(new StringReader(json), RekeyedDocumentKey.class); + type.ensureNoNullsOrThrow(); + } +} From 508ed33a44b709e7702c3e3795375c9bd4518888 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Thu, 29 Jan 2026 13:50:39 -0800 Subject: [PATCH 3/9] Switch to abstract class so it can be package-private --- .../tenantsecurity/kms/v1/BatchDocumentKeys.java | 4 ++-- .../tenantsecurity/kms/v1/DeriveKeyResponse.java | 4 ++-- .../tenantsecurity/kms/v1/NullParsingValidator.java | 7 +++++-- .../tenantsecurity/kms/v1/RekeyedDocumentKey.java | 4 ++-- .../tenantsecurity/kms/v1/UnwrappedDocumentKey.java | 4 ++-- .../tenantsecurity/kms/v1/VoidSecurityEventResponse.java | 4 ++-- .../tenantsecurity/kms/v1/WrappedDocumentKey.java | 4 ++-- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java index c1bf281..7f45c64 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/BatchDocumentKeys.java @@ -7,7 +7,7 @@ * A map from a document ID to a either the wrapped or unwrapped version of a documents keys. Also * includes a map of failures if any problems occurred when performing the batch wrap operation. */ -public class BatchDocumentKeys implements NullParsingValidator { +public class BatchDocumentKeys extends NullParsingValidator { @Key private Map keys; @@ -23,7 +23,7 @@ public Map getFailures() { } @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException { + void ensureNoNullsOrThrow() throws IllegalArgumentException { if (keys == null || failures == null) throw new IllegalArgumentException( "Batch response from the Tenant Security Proxy was not valid."); diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java index 8e944f0..97435ef 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DeriveKeyResponse.java @@ -5,7 +5,7 @@ import com.google.api.client.util.Key; import com.ironcorelabs.tenantsecurity.kms.v1.exception.TspServiceException; -public final class DeriveKeyResponse implements NullParsingValidator { +public final class DeriveKeyResponse extends NullParsingValidator { @Key private boolean hasPrimaryConfig; @Key @@ -39,7 +39,7 @@ CompletableFuture getDerivedKeys(String secretPath, String derivat } @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException { + void ensureNoNullsOrThrow() throws IllegalArgumentException { if (derivedKeys == null) throw new IllegalArgumentException("TSP failed to derive keys."); } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java index 14321b4..dfa5fe7 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/NullParsingValidator.java @@ -1,5 +1,8 @@ package com.ironcorelabs.tenantsecurity.kms.v1; -interface NullParsingValidator { - void ensureNoNullsOrThrow() throws IllegalArgumentException; +abstract class NullParsingValidator { + /** + * Throws an IllegalArgumentException if any of the fields were parsed as null. + */ + abstract void ensureNoNullsOrThrow() throws IllegalArgumentException; } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java index a7c7fb6..48a1005 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/RekeyedDocumentKey.java @@ -5,7 +5,7 @@ /** * An EDEK made by wrapping an existing encrypted document with a tenant's KMS, in Base64 format. */ -public class RekeyedDocumentKey implements NullParsingValidator { +public class RekeyedDocumentKey extends NullParsingValidator { @Key private String edek; @@ -14,7 +14,7 @@ public String getEdek() { } @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException { + void ensureNoNullsOrThrow() throws IllegalArgumentException { if (edek == null) throw new IllegalArgumentException( "Rekeyed document key response from the Tenant Security Proxy was not valid base64."); diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java index ea79c47..6e2d657 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java @@ -7,7 +7,7 @@ /** * Represents the JSON response object from the document/unwrap endpoint which includes the dek. */ -public class UnwrappedDocumentKey implements NullParsingValidator { +public class UnwrappedDocumentKey extends NullParsingValidator { @Key private String dek; @@ -21,7 +21,7 @@ public byte[] getDekBytes() { } @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException { + void ensureNoNullsOrThrow() throws IllegalArgumentException { if (dek == null) throw new IllegalArgumentException( "Unwrap DEK response from the Tenant Security Proxy was not valid base64."); diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java index 1cf18fa..6fb9be2 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java @@ -1,6 +1,6 @@ package com.ironcorelabs.tenantsecurity.kms.v1; -public class VoidSecurityEventResponse implements NullParsingValidator { +public class VoidSecurityEventResponse extends NullParsingValidator { @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException {} + void ensureNoNullsOrThrow() throws IllegalArgumentException {} } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java index 0889f75..93efa9d 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java @@ -7,7 +7,7 @@ /** * A new DEK wrapped by the tenant's KMS and its encrypted form (EDEK), both in Base64 format. */ -public class WrappedDocumentKey implements NullParsingValidator { +public class WrappedDocumentKey extends NullParsingValidator { @Key private String dek; @@ -28,7 +28,7 @@ public String getEdek() { } @Override - public void ensureNoNullsOrThrow() throws IllegalArgumentException { + void ensureNoNullsOrThrow() throws IllegalArgumentException { if (edek == null || dek == null) throw new IllegalArgumentException( "Wrapped document key response from the Tenant Security Proxy was not valid base64."); From 68762ab4abc65927222c931a16fcb482d22af18f Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Thu, 29 Jan 2026 13:55:27 -0800 Subject: [PATCH 4/9] Self review --- .../tenantsecurity/kms/v1/VoidSecurityEventResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java index 6fb9be2..f25511a 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java @@ -1,6 +1,6 @@ package com.ironcorelabs.tenantsecurity.kms.v1; -public class VoidSecurityEventResponse extends NullParsingValidator { +class VoidSecurityEventResponse extends NullParsingValidator { @Override void ensureNoNullsOrThrow() throws IllegalArgumentException {} } From c09a092727aaf96613c955cc0b549b028037ffa9 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Thu, 29 Jan 2026 15:21:27 -0800 Subject: [PATCH 5/9] Address feedback --- .../kms/v1/TenantSecurityRequest.java | 26 ++++++++++++++----- .../kms/v1/UnwrappedDocumentKey.java | 2 +- .../kms/v1/VoidSecurityEventResponse.java | 6 ----- .../kms/v1/WrappedDocumentKey.java | 2 +- .../kms/v1/JsonParsingTest.java | 6 ++--- 5 files changed, 25 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java index e3ea798..d4ede27 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java @@ -164,6 +164,25 @@ private CompletableFuture makeRequestAndPars return parsed; } throw parseFailureFromRequest(resp); + } catch (Exception cause) { + if (cause instanceof TenantSecurityException || cause instanceof IllegalArgumentException) { + throw new CompletionException(cause); + } + throw new CompletionException(new TspServiceException( + TenantSecurityErrorCodes.UNABLE_TO_MAKE_REQUEST, 0, errorMessage, cause)); + } + }, webRequestExecutor); + } + + private CompletableFuture makeRequestAndParseFailure(GenericUrl url, + Map postData, String errorMessage) { + return CompletableFuture.supplyAsync(() -> { + try { + HttpResponse resp = this.getApiRequest(postData, url).execute(); + if (resp.isSuccessStatusCode()) { + return null; + } + throw parseFailureFromRequest(resp); } catch (Exception cause) { if (cause instanceof TenantSecurityException) { throw new CompletionException(cause); @@ -263,12 +282,7 @@ CompletableFuture logSecurityEvent(SecurityEvent event, EventMetadata meta String error = String.format( "Unable to make request to Tenant Security Proxy security event endpoint. Endpoint requested: %s", this.securityEventEndpoint); - return this - .makeRequestAndParseFailure(this.securityEventEndpoint, postData, - VoidSecurityEventResponse.class, error) - // returns a Void - .thenRun(() -> { - }); + return this.makeRequestAndParseFailure(this.securityEventEndpoint, postData, error); } /** diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java index 6e2d657..def5965 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/UnwrappedDocumentKey.java @@ -14,7 +14,7 @@ public class UnwrappedDocumentKey extends NullParsingValidator { public byte[] getDekBytes() { try { return Base64.getDecoder().decode(this.dek); - } catch (Exception e) { + } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "Unwrap DEK response from the Tenant Security Proxy was not valid base64."); } diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java deleted file mode 100644 index f25511a..0000000 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/VoidSecurityEventResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.ironcorelabs.tenantsecurity.kms.v1; - -class VoidSecurityEventResponse extends NullParsingValidator { - @Override - void ensureNoNullsOrThrow() throws IllegalArgumentException {} -} diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java index 93efa9d..e21601e 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/WrappedDocumentKey.java @@ -17,7 +17,7 @@ public class WrappedDocumentKey extends NullParsingValidator { public byte[] getDekBytes() { try { return Base64.getDecoder().decode(this.dek); - } catch (Exception e) { + } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "Wrapped document key response from the Tenant Security Proxy was not valid base64."); } diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java index 39e9fdd..ad6accd 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/JsonParsingTest.java @@ -10,7 +10,7 @@ public class JsonParsingTest { static JsonObjectParser parser = new JsonObjectParser(new GsonFactory()); @Test(expectedExceptions = IllegalArgumentException.class) - void batchKeysAreEmpty() throws Exception { + void batchKeysErrors() throws Exception { String json = "{}"; BatchWrappedDocumentKeys type = parser.parseAndClose( new StringReader(json), BatchWrappedDocumentKeys.class); @@ -18,7 +18,7 @@ void batchKeysAreEmpty() throws Exception { } @Test(expectedExceptions = IllegalArgumentException.class) - void derivedKeyResponseIsEmpty() throws Exception { + void derivedKeyResponseErrors() throws Exception { String json = "{}"; DeriveKeyResponse type = parser.parseAndClose(new StringReader(json), DeriveKeyResponse.class); @@ -43,7 +43,7 @@ void wrappedDocumentKeyErrors() throws Exception { } @Test(expectedExceptions = IllegalArgumentException.class) - void rekeyedDocumentKeyIsNull() throws Exception { + void rekeyedDocumentKeyErrors() throws Exception { String json = "{}"; RekeyedDocumentKey type = parser.parseAndClose(new StringReader(json), RekeyedDocumentKey.class); From 28fd8225747d0bd23765124fe03dc04e0151bfc1 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Fri, 30 Jan 2026 07:58:08 -0800 Subject: [PATCH 6/9] Comment --- .../tenantsecurity/kms/v1/TenantSecurityRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java index d4ede27..65e9cfa 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java @@ -174,6 +174,11 @@ private CompletableFuture makeRequestAndPars }, webRequestExecutor); } + /** + * Overload for generic method for making a request to the provided URL with the provided post + * data. Returns a CompletableFuture because it does not try to parse a successful result. + * In the case of an error, it does try to parse the provided error. + */ private CompletableFuture makeRequestAndParseFailure(GenericUrl url, Map postData, String errorMessage) { return CompletableFuture.supplyAsync(() -> { From 9c4a3fb4777d08d8208be3476716bcfbc4a87375 Mon Sep 17 00:00:00 2001 From: Colt Frederickson Date: Fri, 30 Jan 2026 09:49:57 -0700 Subject: [PATCH 7/9] Add a newline --- test-suites/test-all.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suites/test-all.xml b/test-suites/test-all.xml index 32640a2..dc89c6b 100644 --- a/test-suites/test-all.xml +++ b/test-suites/test-all.xml @@ -15,4 +15,4 @@ - \ No newline at end of file + From 96c1978c8fd1e35a95ffd528a5d342ae06410de5 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Fri, 30 Jan 2026 09:41:45 -0800 Subject: [PATCH 8/9] Missed a file --- .../java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java index dac7692..73b2bd6 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/DerivedKey.java @@ -24,7 +24,7 @@ public DerivedKey() {} byte[] getDerivedKeyBytes() { try { return Base64.getDecoder().decode(this.derivedKey); - } catch (Exception e) { + } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "Derive keys response from the Tenant Security Proxy was not valid base64."); } From 19cd698dfab856c64bd6f894ebc68bd9ffcc8f93 Mon Sep 17 00:00:00 2001 From: Craig Colegrove Date: Fri, 30 Jan 2026 10:01:57 -0800 Subject: [PATCH 9/9] Wrap the IllegalArgumentException --- .../tenantsecurity/kms/v1/TenantSecurityRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java index 65e9cfa..ec88381 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java @@ -165,8 +165,11 @@ private CompletableFuture makeRequestAndPars } throw parseFailureFromRequest(resp); } catch (Exception cause) { - if (cause instanceof TenantSecurityException || cause instanceof IllegalArgumentException) { + if (cause instanceof TenantSecurityException) { throw new CompletionException(cause); + } else if (cause instanceof IllegalArgumentException) { + throw new CompletionException(new TspServiceException( + TenantSecurityErrorCodes.UNKNOWN_ERROR, 0, errorMessage, cause)); } throw new CompletionException(new TspServiceException( TenantSecurityErrorCodes.UNABLE_TO_MAKE_REQUEST, 0, errorMessage, cause));