diff --git a/sdk/pom.xml b/sdk/pom.xml index 64497996..7ba768da 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -16,7 +16,7 @@ 2.1.0 0.7.2 4.12.0 - protocol/go/v0.16.0 + DSPX-2399-platform-proto diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java index 06be4cf6..70d99a68 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -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; @@ -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); } @@ -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); } @@ -93,4 +98,8 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) { public boolean isEc() { return this.curve != null; } + + public boolean isMlkem() { + return this == MLKEM768Key; + } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/MLKEMEncryption.java b/sdk/src/main/java/io/opentdf/platform/sdk/MLKEMEncryption.java new file mode 100644 index 00000000..70bce45c --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/MLKEMEncryption.java @@ -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) { + 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()); + SecretWithEncapsulation swe = kemGen.generateEncapsulated(publicKeyParams); + + byte[] ciphertext = swe.getEncapsulation(); + 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; + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 3ee4ba22..9e77d5ef 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -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; @@ -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(); diff --git a/xtest/sdk/java/cli.sh b/xtest/sdk/java/cli.sh new file mode 100644 index 00000000..d7707600 --- /dev/null +++ b/xtest/sdk/java/cli.sh @@ -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 |encrypt ...|decrypt ...}" >&2 + exit 1 + ;; +esac