Skip to content

feat(sdk): DSPX-3383 add pure ML-KEM-768 and ML-KEM-1024 key wrapping#373

Open
sujankota wants to merge 4 commits into
mainfrom
DSPX-3383-pure-mlkem
Open

feat(sdk): DSPX-3383 add pure ML-KEM-768 and ML-KEM-1024 key wrapping#373
sujankota wants to merge 4 commits into
mainfrom
DSPX-3383-pure-mlkem

Conversation

@sujankota

@sujankota sujankota commented May 29, 2026

Copy link
Copy Markdown
Contributor

Overview

Adds pure ML-KEM-768 (mlkem:768) and ML-KEM-1024 (mlkem:1024) post-quantum key wrapping, on top of the KemProvider SPI introduced by PR #368.

The BC-backed implementation lives in sdk-pqc-bc (already on the classpath for hybrid PQC); the core sdk jar only gets two KeyType constants and an
isMLKEM() predicate. No new BC compile-time references — the FIPS Maven profile still builds cleanly without sdk-pqc-bc.

Conforms to open platform PR opentdf/platform#3562 (replacement for closed #3491).

Wire format

wrappedKey = base64( ASN.1 SEQUENCE { [0] mlkem_ct, [1] AES-GCM(IV(12)||DEK||tag(16)) } )
AES wrap key = mlkem_decaps_shared_secret ← used DIRECTLY, no HKDF
keyAccess.type = "mlkem-wrapped" ← its own KAO scheme
ephemeralPublicKey = absent

Envelope is byte-identical to the hybrid path. Only the wrap-key derivation differs: hybrid still uses HKDF (it's the combiner over two shared-secret
halves); pure ML-KEM uses the 32-byte FIPS 203 Decaps output directly. Rationale: HSM-backed KAS (Thales Luna 7.15.1, strict-FIPS) emits Decaps output only as a
non-extractable CKK_AES object — any KDF in the unwrap chain blocks HSM rewrap. FIPS 203's K is already uniformly random; the "mlkem-wrapped" wire type
itself is the domain-separation tag. See platform ADR
2026-06-16-mlkem-direct-key-wrap.md
.

PEM: standard SPKI / PKCS#8 with NIST FIPS 203 OIDs 2.16.840.1.101.3.4.4.{2,3}.

Changes

File Change
sdk/.../KeyType.java + MLKEM768Key, MLKEM1024Key, isMLKEM()
sdk/.../TDF.java + isMLKEM() dispatch branch → KemProviders.get(kt).wrapDEK(...), kMlkemWrapped constant
sdk-pqc-bc/.../MLKEMAlgorithm.java (new) stateless per-variant params + wrap/unwrap
sdk-pqc-bc/.../MLKEMKeyPair.java (new) keypair holder + PEM I/O
sdk-pqc-bc/.../HybridSpki.java + ML-KEM OID constants
sdk-pqc-bc/.../HybridCrypto.java + 2 cases each in wrap/unwrap dispatch
sdk-pqc-bc/.../BouncyCastleKemProvider.java + 2 entries in SUPPORTED
scripts/test-mlkem.sh (new), scripts/README.md local-KAS round-trip + docs

Plus MLKEMKeyPairTest (10 tests) and TDFMLKEMTest (2 tests).

Verification

Check Result
mvn clean install -DskipTests (default) All 6 modules build
mvn -P fips,!non-fips clean install -DskipTests sdk compiles without sdk-pqc-bc
mvn -pl sdk,sdk-pqc-bc -am test 201 tests, 0 failures

Known gap

Platform proto v0.34.0 doesn't yet define ALGORITHM_MLKEM_768/_1024, so KeyType.fromAlgorithm can't auto-map them. Workaround: --encap-key-type=MLKEM*Key
explicit. Two switch cases when the proto bump lands.

Dependency

Platform PR #3562 is open, not merged — KAS rewrap requires its server-side changes. If reviewers change #3562 before merge, this PR may need a small
follow-up.

Summary by CodeRabbit

  • New Features

    • Added support for pure ML-KEM (FIPS 203) key wrapping to TDF encryption, supporting mlkem:768 and mlkem:1024 variants.
  • Documentation

    • Updated testing documentation with ML-KEM-specific procedures, manifest expectations, and known SDK mapping limitations with workarounds.
  • Tests

    • Added comprehensive test script and unit tests validating ML-KEM key wrapping and round-trip encryption/decryption functionality.

@sujankota sujankota requested review from a team as code owners May 29, 2026 16:57
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc1eb211-d464-4e29-9ad0-22f8f9e6ffea

📥 Commits

Reviewing files that changed from the base of the PR and between c4f8ec0 and f42b226.

📒 Files selected for processing (1)
  • scripts/test-mlkem.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/test-mlkem.sh

📝 Walkthrough

Walkthrough

Adds pure ML-KEM-768 and ML-KEM-1024 wrapping support in the SDK and PQC provider, updates TDF key access generation to emit mlkem-wrapped, adds unit and TDF tests, and introduces a local end-to-end ML-KEM test script with README instructions and workaround notes.

Changes

Pure ML-KEM Post-Quantum Cryptography Support

Layer / File(s) Summary
KeyType enum: MLKEM variants and isMLKEM()
sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java, sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java
Adds ML-KEM key types, an isMLKEM() predicate, and ML-KEM OID constants/documentation for PEM decoding.
MLKEMAlgorithm and MLKEMKeyPair cryptographic implementation
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java, sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPair.java
Introduces pure ML-KEM key generation, PEM conversion, ASN.1 envelope wrapping, and DEK unwrap logic for 768 and 1024 variants.
BouncyCastleKemProvider and HybridCrypto dispatch
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java, sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java
Registers ML-KEM support in the provider and dispatches PQC wrap/unwrap calls to the new ML-KEM implementation.
TDF.createKeyAccess ML-KEM branch
sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Adds mlkem-wrapped key access output and uses KemProviders to populate wrappedKey for ML-KEM TDF creation.
MLKEMKeyPairTest and TDFMLKEMTest
sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java, sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java
Validates PEM round-trips, wrap/unwrap behavior, failure cases, and TDF manifest invariants for ML-KEM-wrapped key access.
test-mlkem.sh end-to-end script and README docs
scripts/test-mlkem.sh, scripts/README.md
Adds a local ML-KEM round-trip script with build, KAS pre-flight, manifest checks, decrypt verification, and matching README usage and workaround guidance.

Sequence Diagram(s)

sequenceDiagram
  participant Script as test-mlkem.sh
  participant KAS as KAS PublicKey API
  participant CLI as Java cmdline
  participant TDF as TDF manifest

  Script->>KAS: Request ML-KEM public key
  KAS-->>Script: SPKI PEM
  Script->>CLI: Encrypt payload with MLKEM key type
  CLI-->>Script: .tdf artifact
  Script->>TDF: Read keyAccess[0]
  TDF-->>Script: mlkem-wrapped metadata
  Script->>CLI: Decrypt artifact
  CLI-->>Script: Plaintext output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • opentdf/java-sdk#368: Adds another KeyType-driven TDF key wrapping path through KemProviders in the same SDK integration points.

Suggested reviewers

  • mkleene
  • marythought

Poem

🐇 I found two keys in moonlit stem,
One marked 768, one 1024 ML-KEM.
They wrapped a secret snug and tight,
Then hopped through TDF by lantern light.
With scripts that sniff and tests that sing,
The burrow hums with quantum spring.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: adding pure ML-KEM-768 and ML-KEM-1024 key wrapping support, which is the core focus across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3383-pure-mlkem

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds pure ML-KEM (FIPS 203) post-quantum key wrapping support for mlkem:768 and mlkem:1024 to the Java SDK, integrating it into the TDF creation flow and adding end-to-end testing scripts. The review feedback recommends optimizing performance by reusing a single static SecureRandom instance, adding defensive null checks in MLKEMKeyPair to prevent potential NullPointerExceptions, and robustly handling trailing slashes in the test script's platform endpoint parsing.

Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment on lines +105 to +108
MLKEMKeyPair generate() {
SecureRandom random = new SecureRandom();
MLKEMKeyPairGenerator gen = new MLKEMKeyPairGenerator();
gen.init(new MLKEMKeyGenerationParameters(random, mlkemParams));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reuse the shared SECURE_RANDOM instance instead of instantiating a new one on every key generation.

Suggested change
MLKEMKeyPair generate() {
SecureRandom random = new SecureRandom();
MLKEMKeyPairGenerator gen = new MLKEMKeyPairGenerator();
gen.init(new MLKEMKeyGenerationParameters(random, mlkemParams));
MLKEMKeyPair generate() {
MLKEMKeyPairGenerator gen = new MLKEMKeyPairGenerator();
gen.init(new MLKEMKeyGenerationParameters(SECURE_RANDOM, mlkemParams));

Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment thread scripts/test-mlkem.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
sdk/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java (1)

114-116: ⚡ Quick win

Assert the recovered DEK actually decrypts the generated TDF.

MLKEMKeyPairTest already proves wrapDEK/unwrapDEK can round-trip a DEK, so this integration test should verify the TDF wiring. On Line 116, checking only .hasSize(32) would still pass if createTDF(...) wrapped the wrong 32-byte buffer. Load the generated TDF with a local unwrap stub and assert the plaintext matches the original payload instead.

As per coding guidelines, "sdk/src/test/java/**/*.java: Prefer focused unit tests over integration tests; keep network calls mocked (e.g., using MockWebServer)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java` around lines 114
- 116, The test currently only asserts the unwrapped DEK byte length; instead,
after obtaining symKey from alg.unwrapDEK(privSeed, wrappedBytes) in
TDFMLKEMTest, use the same local TDF load/decryption path (i.e., call
createTDF(...) output loader or a local unwrap/decrypt helper) to decrypt the
generated TDF payload with symKey and assert the resulting plaintext equals the
original payload bytes; reference the createTDF(...) call that produced the TDF,
the alg.unwrapDEK(...) call that yields symKey, and the wrappedBytes/privSeed
values to wire the decryption and final equality assertion so the test verifies
end-to-end TDF decryption rather than just key size.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/test-mlkem.sh`:
- Around line 121-127: The grpcurl pre-flight is using PLATFORM_ENDPOINT to
build host but the script exposes --kas-url; change the host extraction to use
KAS_URL instead of PLATFORM_ENDPOINT (i.e., replace
host="${PLATFORM_ENDPOINT#http://}"; host="${host#https://"} with host derived
from KAS_URL), so the grpcurl probe that runs in the loop (the resp=$(grpcurl
... kas.AccessService/PublicKey ...) invocation) targets the actual KAS service;
no other logic changes needed (keep the http/https stripping and the
alg_to_string usage).

In `@sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java`:
- Around line 194-207: The method decodeSizedPemBlock currently dereferences pem
without checking for null; add an explicit null-check at the start of
decodeSizedPemBlock(String pem, String expectedType, int expectedSize) and throw
an SDKException (consistent with other parse failures) when pem is null (include
the expectedType in the message for context). This prevents a
NullPointerException and keeps error handling uniform before continuing with
header/footer parsing and base64 decoding.

In `@sdk/src/test/java/io/opentdf/platform/sdk/MLKEMKeyPairTest.java`:
- Around line 27-28: The DEK test vector uses String.getBytes() which depends on
the JVM default charset; update the DEK constant in MLKEMKeyPairTest to call
getBytes with an explicit Charset (e.g.,
"0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.US_ASCII) or
StandardCharsets.UTF_8) and add the necessary import for StandardCharsets so the
test becomes portable across JVMs.
- Around line 90-95: The test tamperedCiphertextFailsAesGcmTag currently asserts
any Exception which is too broad; change it to assert the specific AES-GCM auth
failure by asserting a RuntimeException is thrown from alg.unwrapDEK(...) and
then assert that that RuntimeException's cause is a
javax.crypto.BadPaddingException (or the specific cause type produced by
AesGcm.decrypt). Locate the test method tamperedCiphertextFailsAesGcmTag in
MLKEMKeyPairTest, replace assertThrows(Exception.class, ...) with capturing the
RuntimeException via assertThrows(RuntimeException.class, ...) and add an
assertion that the thrown.getCause() is an instance of
javax.crypto.BadPaddingException. Ensure the assertions reference
alg.unwrapDEK(kp.getPrivateKey(), wrapped) and the AesGcm decrypt failure cause.

---

Nitpick comments:
In `@sdk/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java`:
- Around line 114-116: The test currently only asserts the unwrapped DEK byte
length; instead, after obtaining symKey from alg.unwrapDEK(privSeed,
wrappedBytes) in TDFMLKEMTest, use the same local TDF load/decryption path
(i.e., call createTDF(...) output loader or a local unwrap/decrypt helper) to
decrypt the generated TDF payload with symKey and assert the resulting plaintext
equals the original payload bytes; reference the createTDF(...) call that
produced the TDF, the alg.unwrapDEK(...) call that yields symKey, and the
wrappedBytes/privSeed values to wire the decryption and final equality assertion
so the test verifies end-to-end TDF decryption rather than just key size.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdd21dc4-96e3-458d-adce-22fda003f3eb

📥 Commits

Reviewing files that changed from the base of the PR and between 63b49af and 232a805.

📒 Files selected for processing (8)
  • scripts/README.md
  • scripts/test-mlkem.sh
  • sdk/pom.xml
  • sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
  • sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
  • sdk/src/test/java/io/opentdf/platform/sdk/MLKEMKeyPairTest.java
  • sdk/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java

Comment thread scripts/test-mlkem.sh Outdated
Comment thread sdk/src/main/java/io/opentdf/platform/sdk/MLKEMKeyPair.java Outdated
Comment thread sdk/src/test/java/io/opentdf/platform/sdk/MLKEMKeyPairTest.java Outdated
Comment thread sdk/src/test/java/io/opentdf/platform/sdk/MLKEMKeyPairTest.java Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Pure ML-KEM (FIPS 203) over the same KemProvider SPI introduced by PR #368
for hybrid PQC. The BC-backed implementation lives in sdk-pqc-bc; the core
sdk jar gets two enum values and an isMLKEM() predicate — no BC compile-time
references, so the FIPS profile still builds cleanly without sdk-pqc-bc.

Wire format (matches platform PR #3491):
  wrappedKey = base64(mlkem_ct || AES-GCM(IV(12)||DEK||tag(16)))
  wrap key   = HKDF-SHA256(salt=SHA-256("TDF"), ikm=mlkem_ss, L=32)
  keyAccess.type = "wrapped"  (reuses RSA slot; KAS disambiguates by alg)
  ephemeralPublicKey absent

PEM is standard SPKI/PKCS#8 with the NIST FIPS 203 OIDs
(2.16.840.1.101.3.4.4.{2,3}) inside the AlgorithmIdentifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

X-Test Failure Report

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java (1)

106-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow tamper-path assertion to the unwrap contract.

Line 106 uses assertThrows(Exception.class), which can pass on unrelated failures. Assert the concrete unwrap failure type (SDKException) so this test only validates auth-failure behavior.

Proposed change
-        assertThrows(Exception.class,
+        assertThrows(SDKException.class,
                 () -> algo.unwrapDEK(kp.getPrivateKey(), wrapped),
                 "tampered AES-GCM tag must not unwrap");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java`
around lines 106 - 108, The test assertion in the unwrapDEK test case uses the
overly broad Exception.class type in assertThrows, which allows the test to pass
even if unrelated failures occur instead of the expected authentication failure.
Change the assertThrows call to specify the concrete exception type SDKException
instead of Exception.class. This ensures the test validates the specific unwrap
contract failure for tampered AES-GCM authentication tags rather than catching
any exception that might be thrown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java`:
- Around line 27-42: The supportedKeyTypes() method in BouncyCastleKemProvider
returns the static SUPPORTED EnumSet directly, allowing callers to mutate it and
affect provider capabilities for all future lookups. Modify the return statement
to wrap SUPPORTED in Collections.unmodifiableSet() or use Set.copyOf() to return
an immutable copy instead of exposing the mutable EnumSet directly.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java`:
- Around line 119-124: The addition of c.pos and seqLen can cause integer
overflow before the buffer bounds comparison is performed, allowing malformed
ASN.1 envelopes with large lengths to bypass validation. Check for overflow
before calculating seqEnd by verifying that c.pos does not exceed
Integer.MAX_VALUE minus seqLen. Apply the same overflow-safe validation pattern
to the similar calculation at line 142-146 where c.pos and len are combined.
This ensures that integer wraparound cannot be exploited to bypass buffer bounds
checks.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java`:
- Around line 105-116: The catch block in the decodeSpkiPem method only catches
IOException, but BouncyCastle's getInstance() methods can also throw unchecked
IllegalArgumentException when the ASN.1 structure is malformed. Modify the catch
clause to catch both IOException and IllegalArgumentException together (e.g.,
catch (IOException | IllegalArgumentException e)), ensuring both exceptions are
wrapped and re-thrown as SDKException with appropriate error context. Apply this
same fix to the two other similar decode methods at lines 123-134 and 154-160.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java`:
- Around line 148-160: The ML-KEM encapsulation handling in MLKEMAlgorithm
leaves sensitive secret material in memory because SecretWithEncapsulation is
never destroyed. Update the encrypt/marshal flow around the generateEncapsulated
call to use try/finally, ensure enc.destroy() is always invoked, and zeroize the
local wrapKey buffer with Arrays.fill after it has been used by AesGcm. Keep the
existing ciphertext and size checks in place, but make sure cleanup happens
regardless of success or failure.

---

Nitpick comments:
In
`@sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java`:
- Around line 106-108: The test assertion in the unwrapDEK test case uses the
overly broad Exception.class type in assertThrows, which allows the test to pass
even if unrelated failures occur instead of the expected authentication failure.
Change the assertThrows call to specify the concrete exception type SDKException
instead of Exception.class. This ensures the test validates the specific unwrap
contract failure for tampered AES-GCM authentication tags rather than catching
any exception that might be thrown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7ae0bef-93b9-4c00-a448-4ccf1dfccfb9

📥 Commits

Reviewing files that changed from the base of the PR and between 232a805 and c4f8ec0.

📒 Files selected for processing (11)
  • scripts/README.md
  • scripts/test-mlkem.sh
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPair.java
  • sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java
  • sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java
  • sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (1)
sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java (1)

106-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow tamper-path assertion to the unwrap contract.

Line 106 uses assertThrows(Exception.class), which can pass on unrelated failures. Assert the concrete unwrap failure type (SDKException) so this test only validates auth-failure behavior.

Proposed change
-        assertThrows(Exception.class,
+        assertThrows(SDKException.class,
                 () -> algo.unwrapDEK(kp.getPrivateKey(), wrapped),
                 "tampered AES-GCM tag must not unwrap");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java`
around lines 106 - 108, The test assertion in the unwrapDEK test case uses the
overly broad Exception.class type in assertThrows, which allows the test to pass
even if unrelated failures occur instead of the expected authentication failure.
Change the assertThrows call to specify the concrete exception type SDKException
instead of Exception.class. This ensures the test validates the specific unwrap
contract failure for tampered AES-GCM authentication tags rather than catching
any exception that might be thrown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java`:
- Around line 27-42: The supportedKeyTypes() method in BouncyCastleKemProvider
returns the static SUPPORTED EnumSet directly, allowing callers to mutate it and
affect provider capabilities for all future lookups. Modify the return statement
to wrap SUPPORTED in Collections.unmodifiableSet() or use Set.copyOf() to return
an immutable copy instead of exposing the mutable EnumSet directly.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java`:
- Around line 119-124: The addition of c.pos and seqLen can cause integer
overflow before the buffer bounds comparison is performed, allowing malformed
ASN.1 envelopes with large lengths to bypass validation. Check for overflow
before calculating seqEnd by verifying that c.pos does not exceed
Integer.MAX_VALUE minus seqLen. Apply the same overflow-safe validation pattern
to the similar calculation at line 142-146 where c.pos and len are combined.
This ensures that integer wraparound cannot be exploited to bypass buffer bounds
checks.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java`:
- Around line 105-116: The catch block in the decodeSpkiPem method only catches
IOException, but BouncyCastle's getInstance() methods can also throw unchecked
IllegalArgumentException when the ASN.1 structure is malformed. Modify the catch
clause to catch both IOException and IllegalArgumentException together (e.g.,
catch (IOException | IllegalArgumentException e)), ensuring both exceptions are
wrapped and re-thrown as SDKException with appropriate error context. Apply this
same fix to the two other similar decode methods at lines 123-134 and 154-160.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java`:
- Around line 148-160: The ML-KEM encapsulation handling in MLKEMAlgorithm
leaves sensitive secret material in memory because SecretWithEncapsulation is
never destroyed. Update the encrypt/marshal flow around the generateEncapsulated
call to use try/finally, ensure enc.destroy() is always invoked, and zeroize the
local wrapKey buffer with Arrays.fill after it has been used by AesGcm. Keep the
existing ciphertext and size checks in place, but make sure cleanup happens
regardless of success or failure.

---

Nitpick comments:
In
`@sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java`:
- Around line 106-108: The test assertion in the unwrapDEK test case uses the
overly broad Exception.class type in assertThrows, which allows the test to pass
even if unrelated failures occur instead of the expected authentication failure.
Change the assertThrows call to specify the concrete exception type SDKException
instead of Exception.class. This ensures the test validates the specific unwrap
contract failure for tampered AES-GCM authentication tags rather than catching
any exception that might be thrown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7ae0bef-93b9-4c00-a448-4ccf1dfccfb9

📥 Commits

Reviewing files that changed from the base of the PR and between 232a805 and c4f8ec0.

📒 Files selected for processing (11)
  • scripts/README.md
  • scripts/test-mlkem.sh
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java
  • sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPair.java
  • sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/TDFMLKEMTest.java
  • sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/MLKEMKeyPairTest.java
  • sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
🛑 Comments failed to post (4)
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java (1)

27-42: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not expose the mutable EnumSet.

supportedKeyTypes() returns the static EnumSet directly, so any caller can mutate provider capabilities for all future lookups. Return an immutable set or a defensive copy.

Proposed fix
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
@@
-    private static final Set<KeyType> SUPPORTED = EnumSet.of(
+    private static final Set<KeyType> SUPPORTED = Collections.unmodifiableSet(EnumSet.of(
             KeyType.HybridXWingKey,
             KeyType.HybridSecp256r1MLKEM768Key,
             KeyType.HybridSecp384r1MLKEM1024Key,
             KeyType.MLKEM768Key,
-            KeyType.MLKEM1024Key);
+            KeyType.MLKEM1024Key));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java`
around lines 27 - 42, The supportedKeyTypes() method in BouncyCastleKemProvider
returns the static SUPPORTED EnumSet directly, allowing callers to mutate it and
affect provider capabilities for all future lookups. Modify the return statement
to wrap SUPPORTED in Collections.unmodifiableSet() or use Set.copyOf() to return
an immutable copy instead of exposing the mutable EnumSet directly.
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java (1)

119-124: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use overflow-safe ASN.1 length bounds checks.

c.pos + seqLen and c.pos + len can overflow before the buffer comparison. For malformed long-form lengths, the element path can then attempt a huge allocation instead of rejecting the envelope.

Proposed fix
         }
         int seqLen = readLength(c);
-        int seqEnd = c.pos + seqLen;
-        if (seqEnd > der.length) {
+        if (seqLen > der.length - c.pos) {
             throw new SDKException("hybrid wrapped key envelope length exceeds buffer");
         }
+        int seqEnd = c.pos + seqLen;
         if (seqEnd != der.length) {
             throw new SDKException("hybrid wrapped key envelope has trailing bytes");
         }
@@
         }
         int len = readLength(c);
-        if (c.pos + len > c.buf.length) {
+        if (len > c.buf.length - c.pos) {
             throw new SDKException("context-tagged element length exceeds buffer");
         }

Also applies to: 142-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridCrypto.java`
around lines 119 - 124, The addition of c.pos and seqLen can cause integer
overflow before the buffer bounds comparison is performed, allowing malformed
ASN.1 envelopes with large lengths to bypass validation. Check for overflow
before calculating seqEnd by verifying that c.pos does not exceed
Integer.MAX_VALUE minus seqLen. Apply the same overflow-safe validation pattern
to the similar calculation at line 142-146 where c.pos and len are combined.
This ensures that integer wraparound cannot be exploited to bypass buffer bounds
checks.
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java (1)

105-116: 🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "HybridSpki.java" --absolute

Repository: opentdf/java-sdk

Length of output: 378


🏁 Script executed:

git ls-files "sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java"

Repository: opentdf/java-sdk

Length of output: 226


🏁 Script executed:

cat -n sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java | sed -n '100,165p'

Repository: opentdf/java-sdk

Length of output: 3538


🌐 Web query:

BouncyCastle ASN1Primitive.fromByteArray IllegalArgumentException exception handling

💡 Result:

The method org.bouncycastle.asn1.ASN1Primitive.fromByteArray(byte[] data) is designed to parse well-formed ASN.1 DER data and is explicitly documented to throw a java.io.IOException when it encounters parsing issues [1][2][3]. When an IllegalArgumentException occurs in conjunction with this method, it typically originates from deeper validation logic within the BouncyCastle ASN.1 classes (such as ASN1Integer, ASN1Boolean, or ASN1ObjectIdentifier) during the reconstruction of primitive objects [4][5][6]. Handling this Exception 1. Verify Data Integrity: The most common cause is malformed or non-compliant ASN.1 data (e.g., integers not encoded in the smallest possible octets, invalid OID encodings, or incorrect boolean lengths) [4][5][6]. Ideally, the source of the data should be corrected to adhere to ASN.1/DER encoding rules [4]. 2. Use System Properties for Workarounds: For cases where you must process non-compliant data that cannot be fixed at the source, BouncyCastle provides system properties to relax validation rules for specific known issues [4][6]. These properties can be set to "true" to bypass certain strict checks: - org.bouncycastle.asn1.allow_unsafe_integer: Allows non-minimal integer encodings [4]. - org.bouncycastle.asn1.allow_wrong_oid_enc: Allows specific invalid OID encodings [6]. If possible, these properties should be applied locally using org.bouncycastle.util.Properties.setThreadOverride(property, value) to minimize security risks and avoid affecting the entire application [4]. 3. Proper Exception Catching: Because ASN1Primitive.fromByteArray declares that it throws IOException, you should ensure your application correctly handles IOException [1][2]. If your code also encounters IllegalArgumentException (which is a RuntimeException) during this process, it often indicates an underlying violation of DER encoding rules that BouncyCastle's strict internal validation cannot parse [4][5][6]. Catching IllegalArgumentException directly is sometimes necessary when dealing with unpredictable or legacy third-party data, but it is best practice to investigate the specific cause (e.g., corrupted stream, invalid object type) to ensure it is safe to proceed [4][7][8]. Important Note: Always use consistent versions of BouncyCastle provider jars to avoid classpath conflicts, which can also trigger unexpected IllegalArgumentExceptions during object instantiation [7].

Citations:


🌐 Web query:

BouncyCastle SubjectPublicKeyInfo.getInstance() throws exceptions IllegalArgumentException

💡 Result:

The java.lang.IllegalArgumentException thrown by Bouncy Castle's SubjectPublicKeyInfo.getInstance typically indicates that the provided input object is not a valid ASN.1 encoded structure, or it does not conform to the expected X.509 SubjectPublicKeyInfo sequence [1]. Common causes include: 1. Invalid Input Data: The input byte array may be corrupted, incomplete, or formatted in a non-ASN.1 format (e.g., raw key material, SSH public key format, or X/Y coordinates without the required OID and metadata headers) [2][3]. SubjectPublicKeyInfo expects an ASN.1 encoded sequence containing both the AlgorithmIdentifier and the subjectPublicKey (bit string) [1]. 2. Incorrect Data Type: Passing an object that is not an ASN.1 sequence (e.g., just the raw key bits or a different ASN.1 structure) will trigger a failure during decoding [3][4]. 3. Missing Metadata: If you are attempting to create a SubjectPublicKeyInfo from raw public key bytes (like EC coordinates), you must manually wrap the raw bytes with the appropriate AlgorithmIdentifier (OID) and structure rather than passing the raw bytes directly to getInstance [5][3]. To resolve this, ensure that: - The byte array passed to getInstance is a DER/BER encoded SubjectPublicKeyInfo structure, such as that returned by PublicKey.getEncoded from standard Java JCA objects [2][4]. - If you have raw key material, use the appropriate Bouncy Castle factory methods (e.g., PublicKeyFactory.createKey) or explicitly construct a new SubjectPublicKeyInfo instance using the correct AlgorithmIdentifier and key data [5][3]. You can inspect the contents of your byte array to verify it starts with an ASN.1 Sequence tag (0x30) before attempting to parse it [1].

Citations:


Catch IllegalArgumentException alongside IOException in ASN.1 decode methods.

BouncyCastle's getInstance() methods (SubjectPublicKeyInfo, PrivateKeyInfo, ECPrivateKey) throw unchecked IllegalArgumentException when the input is not a valid ASN.1 encoded structure or doesn't conform to the expected format. The current exception handlers only catch IOException, allowing these unchecked exceptions to escape and violate the SDK's error contract. Add IllegalArgumentException to all three catch blocks to normalize malformed DER errors to SDKException.

Proposed fix
-        } catch (IOException e) {
+        } catch (IOException | IllegalArgumentException e) {
             throw new SDKException("failed to parse SPKI", e);
         }
@@
-        } catch (IOException e) {
+        } catch (IOException | IllegalArgumentException e) {
             throw new SDKException("failed to parse PKCS#8", e);
         }
@@
-        } catch (IOException e) {
+        } catch (IOException | IllegalArgumentException e) {
             throw new SDKException("failed to parse RFC 5915 ECPrivateKey", e);
         }

Applies to lines 105–116, 123–134, and 154–160.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/HybridSpki.java`
around lines 105 - 116, The catch block in the decodeSpkiPem method only catches
IOException, but BouncyCastle's getInstance() methods can also throw unchecked
IllegalArgumentException when the ASN.1 structure is malformed. Modify the catch
clause to catch both IOException and IllegalArgumentException together (e.g.,
catch (IOException | IllegalArgumentException e)), ensuring both exceptions are
wrapped and re-thrown as SDKException with appropriate error context. Apply this
same fix to the two other similar decode methods at lines 123-134 and 154-160.
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java (1)

148-160: 🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MLKEMAlgorithm.java" | head -5

Repository: opentdf/java-sdk

Length of output: 232


🏁 Script executed:

# Once we find the file, check its content around lines 148-160
if [ -f "sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java" ]; then
  echo "=== File exists, checking line range 140-170 ===" 
  sed -n '140,170p' "sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java"
fi

Repository: opentdf/java-sdk

Length of output: 2024


🏁 Script executed:

# Check the full structure of the file to understand imports and class definition
ast-grep outline "sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java" --view expanded | head -100

Repository: opentdf/java-sdk

Length of output: 1111


🌐 Web query:

BouncyCastle SecretWithEncapsulation Destroyable interface lifecycle management

💡 Result:

In the Bouncy Castle cryptography library, the SecretWithEncapsulation interface is used to handle secret keys generated during encapsulation processes, such as in Key Encapsulation Mechanisms (KEM) [1][2]. This interface extends the javax.security.auth.Destroyable interface [1][3], which provides the standard Java mechanism for lifecycle management of sensitive objects [1][4]. Lifecycle Management via Destroyable: The Destroyable interface allows implementing classes to explicitly release sensitive data to mitigate security risks associated with keeping secrets in memory [1][5]. 1. destroy: Calling this method signals the object to destroy its sensitive contents (typically by zeroing out the underlying byte arrays) [5][6]. Once destroyed, attempts to use the object may result in an IllegalStateException or equivalent [5]. 2. isDestroyed: This method allows callers to check whether the object has already been destroyed, which is crucial for safe memory management and preventing use-after-destroy errors [5]. Implementation Details: Classes implementing SecretWithEncapsulation, such as SecretWithEncapsulationImpl, are responsible for overriding the destroy method to ensure that both the secret key and any associated sensitive data are cleared from memory [5]. When managing the lifecycle, developers should treat these objects as temporary, calling destroy as soon as the secret is no longer needed (e.g., after it has been used to derive a session key or perform further cryptographic operations). Because SecretWithEncapsulation extends Destroyable, it aligns with broader Bouncy Castle practices, where various sensitive key types (such as private keys) also implement Destroyable to support proactive memory zeroization [7][8]. In specific FIPS-compliant contexts, Bouncy Castle may also offer specialized interfaces like ZeroizableSecretKey for more explicit, manual zeroization control [6].

Citations:


Destroy the encapsulation secret after use.

SecretWithEncapsulation extends Destroyable per BouncyCastle's API contract. The current code extracts the secret and encapsulation but never calls destroy() on enc, leaving sensitive material in memory until garbage collection. Wrap the encryption/marshal logic in try/finally, call enc.destroy(), and zeroize the local wrapKey copy with Arrays.fill() after use.

Proposed fix
         MLKEMPublicKeyParameters pub = new MLKEMPublicKeyParameters(mlkemParams, rawPub);
         SecretWithEncapsulation enc = new MLKEMGenerator(SECURE_RANDOM).generateEncapsulated(pub);
-        byte[] wrapKey = enc.getSecret();           // 32-byte AES key, used directly (no HKDF)
-        byte[] ciphertext = enc.getEncapsulation();
-        if (ciphertext.length != ciphertextSize) {
-            throw new SDKException("ML-KEM ciphertext size " + ciphertext.length + " != expected " + ciphertextSize);
-        }
-        if (wrapKey.length != HybridCrypto.WRAP_KEY_SIZE) {
-            throw new SDKException("ML-KEM shared secret size " + wrapKey.length
-                    + " != expected " + HybridCrypto.WRAP_KEY_SIZE);
-        }
-        byte[] encryptedDek = new AesGcm(wrapKey).encrypt(dek).asBytes();
-        return HybridCrypto.marshalEnvelope(ciphertext, encryptedDek);
+        byte[] wrapKey = enc.getSecret();           // 32-byte AES key, used directly (no HKDF)
+        try {
+            byte[] ciphertext = enc.getEncapsulation();
+            if (ciphertext.length != ciphertextSize) {
+                throw new SDKException("ML-KEM ciphertext size " + ciphertext.length + " != expected " + ciphertextSize);
+            }
+            if (wrapKey.length != HybridCrypto.WRAP_KEY_SIZE) {
+                throw new SDKException("ML-KEM shared secret size " + wrapKey.length
+                        + " != expected " + HybridCrypto.WRAP_KEY_SIZE);
+            }
+            byte[] encryptedDek = new AesGcm(wrapKey).encrypt(dek).asBytes();
+            return HybridCrypto.marshalEnvelope(ciphertext, encryptedDek);
+        } finally {
+            java.util.Arrays.fill(wrapKey, (byte) 0);
+            enc.destroy();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        MLKEMPublicKeyParameters pub = new MLKEMPublicKeyParameters(mlkemParams, rawPub);
        SecretWithEncapsulation enc = new MLKEMGenerator(SECURE_RANDOM).generateEncapsulated(pub);
        byte[] wrapKey = enc.getSecret();           // 32-byte AES key, used directly (no HKDF)
        try {
            byte[] ciphertext = enc.getEncapsulation();
            if (ciphertext.length != ciphertextSize) {
                throw new SDKException("ML-KEM ciphertext size " + ciphertext.length + " != expected " + ciphertextSize);
            }
            if (wrapKey.length != HybridCrypto.WRAP_KEY_SIZE) {
                throw new SDKException("ML-KEM shared secret size " + wrapKey.length
                         " != expected " + HybridCrypto.WRAP_KEY_SIZE);
            }
            byte[] encryptedDek = new AesGcm(wrapKey).encrypt(dek).asBytes();
            return HybridCrypto.marshalEnvelope(ciphertext, encryptedDek);
        } finally {
            java.util.Arrays.fill(wrapKey, (byte) 0);
            enc.destroy();
        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/MLKEMAlgorithm.java`
around lines 148 - 160, The ML-KEM encapsulation handling in MLKEMAlgorithm
leaves sensitive secret material in memory because SecretWithEncapsulation is
never destroyed. Update the encrypt/marshal flow around the generateEncapsulated
call to use try/finally, ensure enc.destroy() is always invoked, and zeroize the
local wrapKey buffer with Arrays.fill after it has been used by AesGcm. Keep the
existing ciphertext and size checks in place, but make sure cleanup happens
regardless of success or failure.

@github-actions

Copy link
Copy Markdown
Contributor

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants