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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<kotlin.version>2.1.0</kotlin.version>
<connect.version>0.7.2</connect.version>
<okhttp.version>4.12.0</okhttp.version>
<platform.branch>protocol/go/v0.16.0</platform.branch>
<platform.branch>DSPX-2399-platform-proto</platform.branch>
</properties>
<dependencies>
<!-- Logging Dependencies -->
Expand Down
11 changes: 10 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum KeyType {
RSA4096Key("rsa:4096"),
EC256Key("ec:secp256r1", SECP256R1),
EC384Key("ec:secp384r1", SECP384R1),
EC521Key("ec:secp521r1", SECP521R1);
EC521Key("ec:secp521r1", SECP521R1),
MLKEM768Key("mlkem:768");

private final String keyType;
private final ECCurve curve;
Expand Down Expand Up @@ -65,6 +66,8 @@ public static KeyType fromAlgorithm(Algorithm algorithm) {
return KeyType.EC384Key;
case ALGORITHM_EC_P521:
return KeyType.EC521Key;
case ALGORITHM_ML_KEM_768:
return KeyType.MLKEM768Key;
default:
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
Expand All @@ -85,6 +88,8 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) {
return KeyType.EC384Key;
case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1:
return KeyType.EC521Key;
case KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768:
return KeyType.MLKEM768Key;
default:
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
Expand All @@ -93,4 +98,8 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) {
public boolean isEc() {
return this.curve != null;
}

public boolean isMlkem() {
return this == MLKEM768Key;
}
}
60 changes: 60 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/MLKEMEncryption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.opentdf.platform.sdk;

import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.SecretWithEncapsulation;
import org.bouncycastle.crypto.util.PublicKeyFactory;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;

import java.io.IOException;
import java.io.StringReader;
import java.security.SecureRandom;

/**
* Handles ML-KEM-768 key encapsulation for wrapping a symmetric DEK.
*
* Wire format: base64(ml_kem_ciphertext [1088 bytes] || aes_gcm_wrapped_dek)
* No ephemeralPublicKey field; KeyAccess type is "wrapped".
*/
class MLKEMEncryption {

/** ML-KEM-768 ciphertext is always 1088 bytes. */
static final int CIPHERTEXT_SIZE = 1088;

private final MLKEMPublicKeyParameters publicKeyParams;

MLKEMEncryption(String pemPublicKey) {
try {
PEMParser parser = new PEMParser(new StringReader(pemPublicKey));
SubjectPublicKeyInfo spki = (SubjectPublicKeyInfo) parser.readObject();
parser.close();
publicKeyParams = (MLKEMPublicKeyParameters) PublicKeyFactory.createKey(spki);
} catch (IOException e) {
Comment on lines +25 to +33
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

Improve resource management by using try-with-resources for PEMParser to ensure it is closed even if an exception occurs during key parsing. Additionally, performance can be optimized by reusing a static SecureRandom instance instead of creating a new one for every encapsulation operation.

Suggested change
private final MLKEMPublicKeyParameters publicKeyParams;
MLKEMEncryption(String pemPublicKey) {
try {
PEMParser parser = new PEMParser(new StringReader(pemPublicKey));
SubjectPublicKeyInfo spki = (SubjectPublicKeyInfo) parser.readObject();
parser.close();
publicKeyParams = (MLKEMPublicKeyParameters) PublicKeyFactory.createKey(spki);
} catch (IOException e) {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private final MLKEMPublicKeyParameters publicKeyParams;
MLKEMEncryption(String pemPublicKey) {
try (PEMParser parser = new PEMParser(new StringReader(pemPublicKey))) {
SubjectPublicKeyInfo spki = (SubjectPublicKeyInfo) parser.readObject();
publicKeyParams = (MLKEMPublicKeyParameters) PublicKeyFactory.createKey(spki);
} catch (IOException e) {

throw new SDKException("error parsing ML-KEM-768 public key", e);
} catch (ClassCastException e) {
throw new SDKException("public key is not an ML-KEM key", e);
}
}

/**
* Encapsulates against the KAS ML-KEM-768 public key and AES-GCM wraps the DEK.
*
* @return ciphertext (1088 bytes) concatenated with the AES-GCM wrapped DEK
*/
byte[] encapsulateAndWrap(byte[] dek) {
MLKEMGenerator kemGen = new MLKEMGenerator(new SecureRandom());
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

Use the static SECURE_RANDOM instance to avoid the overhead of instantiating a new SecureRandom on every call.

Suggested change
MLKEMGenerator kemGen = new MLKEMGenerator(new SecureRandom());
MLKEMGenerator kemGen = new MLKEMGenerator(SECURE_RANDOM);

SecretWithEncapsulation swe = kemGen.generateEncapsulated(publicKeyParams);

byte[] ciphertext = swe.getEncapsulation();
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

Leverage the defined CIPHERTEXT_SIZE constant to validate that the generated encapsulation matches the expected length for ML-KEM-768.

Suggested change
byte[] ciphertext = swe.getEncapsulation();
byte[] ciphertext = swe.getEncapsulation();
if (ciphertext.length != CIPHERTEXT_SIZE) {
throw new SDKException("invalid ML-KEM-768 ciphertext length: " + ciphertext.length);
}

byte[] sharedSecret = swe.getSecret();

byte[] sessionKey = ECKeyPair.calculateHKDF(TDF.GLOBAL_KEY_SALT, sharedSecret);
byte[] aesWrappedDek = new AesGcm(sessionKey).encrypt(dek).asBytes();

byte[] combined = new byte[ciphertext.length + aesWrappedDek.length];
System.arraycopy(ciphertext, 0, combined, 0, ciphertext.length);
System.arraycopy(aesWrappedDek, 0, combined, ciphertext.length, aesWrappedDek.length);
return combined;
}
}
8 changes: 8 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA
keyAccess.wrappedKey = ecKeyWrappedKeyInfo.wrappedKey;
keyAccess.ephemeralPublicKey = ecKeyWrappedKeyInfo.publicKey;
keyAccess.keyType = kECWrapped;
} else if (keyType.isMlkem()) {
keyAccess.wrappedKey = createMLKEMWrappedKey(kasInfo, symKey);
keyAccess.keyType = kWrapped;
} else {
keyAccess.wrappedKey = createRSAWrappedKey(kasInfo, symKey);
keyAccess.keyType = kWrapped;
Expand Down Expand Up @@ -264,6 +267,11 @@ private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) {
byte[] wrappedKey = asymEncrypt.encrypt(symKey);
return Base64.getEncoder().encodeToString(wrappedKey);
}

private String createMLKEMWrappedKey(Config.KASInfo kasInfo, byte[] symKey) {
MLKEMEncryption mlkem = new MLKEMEncryption(kasInfo.PublicKey);
return Base64.getEncoder().encodeToString(mlkem.encapsulateAndWrap(symKey));
}
}

private static final Base64.Decoder decoder = Base64.getDecoder();
Expand Down
39 changes: 39 additions & 0 deletions xtest/sdk/java/cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Cross-SDK test CLI helper for the OpenTDF Java SDK.
# Called by the xtest harness to check feature support and run encrypt/decrypt ops.
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
JAR="${REPO_ROOT}/cmdline/target/cmdline.jar"

_jar_help() {
java -jar "${JAR}" "$@" --help 2>&1 || true
}

case "${1:-}" in
supports)
feature="${2:-}"
case "$feature" in
mechanism-mlkem)
# mlkem:768 is a valid --encap-key-type value; picocli lists it in the
# encrypt help as a COMPLETION-CANDIDATE from KeyType.MLKEM768Key.toString()
_jar_help encrypt | grep -q "mlkem:768"
;;
*)
exit 1
;;
esac
;;
encrypt)
shift
java -jar "${JAR}" encrypt "$@"
;;
decrypt)
shift
java -jar "${JAR}" decrypt "$@"
;;
*)
echo "usage: $0 {supports <feature>|encrypt ...|decrypt ...}" >&2
exit 1
;;
esac
Loading