Skip to content

Commit 60df059

Browse files
feat(java-sdk): add ML-KEM-768 post-quantum key encapsulation (DSPX-2399)
- Add MLKEM768Key("mlkem:768") enum entry to KeyType with fromAlgorithm and fromPublicKeyAlgorithm mappings for ALGORITHM_ML_KEM_768 / KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 - Implement MLKEMEncryption using Bouncy Castle pqc; wire format is base64(ml_kem_ciphertext [1088 bytes] || aes_gcm_wrapped_dek) with no ephemeralPublicKey field; KAO type "wrapped" - Wire MLKEMEncryption into TDF.createKeyAccess via new isMlkem() branch - Point sdk/pom.xml platform.branch at DSPX-2399-platform-proto to pull in KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 = 13 proto stubs - Add xtest/sdk/java/cli.sh supports mechanism-mlkem case Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9991b07 commit 60df059

5 files changed

Lines changed: 118 additions & 2 deletions

File tree

sdk/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<kotlin.version>2.1.0</kotlin.version>
1717
<connect.version>0.7.2</connect.version>
1818
<okhttp.version>4.12.0</okhttp.version>
19-
<platform.branch>protocol/go/v0.16.0</platform.branch>
19+
<platform.branch>DSPX-2399-platform-proto</platform.branch>
2020
</properties>
2121
<dependencies>
2222
<!-- Logging Dependencies -->

sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public enum KeyType {
1414
RSA4096Key("rsa:4096"),
1515
EC256Key("ec:secp256r1", SECP256R1),
1616
EC384Key("ec:secp384r1", SECP384R1),
17-
EC521Key("ec:secp521r1", SECP521R1);
17+
EC521Key("ec:secp521r1", SECP521R1),
18+
MLKEM768Key("mlkem:768");
1819

1920
private final String keyType;
2021
private final ECCurve curve;
@@ -65,6 +66,8 @@ public static KeyType fromAlgorithm(Algorithm algorithm) {
6566
return KeyType.EC384Key;
6667
case ALGORITHM_EC_P521:
6768
return KeyType.EC521Key;
69+
case ALGORITHM_ML_KEM_768:
70+
return KeyType.MLKEM768Key;
6871
default:
6972
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
7073
}
@@ -85,6 +88,8 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) {
8588
return KeyType.EC384Key;
8689
case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1:
8790
return KeyType.EC521Key;
91+
case KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768:
92+
return KeyType.MLKEM768Key;
8893
default:
8994
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
9095
}
@@ -93,4 +98,8 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) {
9398
public boolean isEc() {
9499
return this.curve != null;
95100
}
101+
102+
public boolean isMlkem() {
103+
return this == MLKEM768Key;
104+
}
96105
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
4+
import org.bouncycastle.crypto.SecretWithEncapsulation;
5+
import org.bouncycastle.crypto.util.PublicKeyFactory;
6+
import org.bouncycastle.openssl.PEMParser;
7+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator;
8+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
9+
10+
import java.io.IOException;
11+
import java.io.StringReader;
12+
import java.security.SecureRandom;
13+
14+
/**
15+
* Handles ML-KEM-768 key encapsulation for wrapping a symmetric DEK.
16+
*
17+
* Wire format: base64(ml_kem_ciphertext [1088 bytes] || aes_gcm_wrapped_dek)
18+
* No ephemeralPublicKey field; KeyAccess type is "wrapped".
19+
*/
20+
class MLKEMEncryption {
21+
22+
/** ML-KEM-768 ciphertext is always 1088 bytes. */
23+
static final int CIPHERTEXT_SIZE = 1088;
24+
25+
private final MLKEMPublicKeyParameters publicKeyParams;
26+
27+
MLKEMEncryption(String pemPublicKey) {
28+
try {
29+
PEMParser parser = new PEMParser(new StringReader(pemPublicKey));
30+
SubjectPublicKeyInfo spki = (SubjectPublicKeyInfo) parser.readObject();
31+
parser.close();
32+
publicKeyParams = (MLKEMPublicKeyParameters) PublicKeyFactory.createKey(spki);
33+
} catch (IOException e) {
34+
throw new SDKException("error parsing ML-KEM-768 public key", e);
35+
} catch (ClassCastException e) {
36+
throw new SDKException("public key is not an ML-KEM key", e);
37+
}
38+
}
39+
40+
/**
41+
* Encapsulates against the KAS ML-KEM-768 public key and AES-GCM wraps the DEK.
42+
*
43+
* @return ciphertext (1088 bytes) concatenated with the AES-GCM wrapped DEK
44+
*/
45+
byte[] encapsulateAndWrap(byte[] dek) {
46+
MLKEMGenerator kemGen = new MLKEMGenerator(new SecureRandom());
47+
SecretWithEncapsulation swe = kemGen.generateEncapsulated(publicKeyParams);
48+
49+
byte[] ciphertext = swe.getEncapsulation();
50+
byte[] sharedSecret = swe.getSecret();
51+
52+
byte[] sessionKey = ECKeyPair.calculateHKDF(TDF.GLOBAL_KEY_SALT, sharedSecret);
53+
byte[] aesWrappedDek = new AesGcm(sessionKey).encrypt(dek).asBytes();
54+
55+
byte[] combined = new byte[ciphertext.length + aesWrappedDek.length];
56+
System.arraycopy(ciphertext, 0, combined, 0, ciphertext.length);
57+
System.arraycopy(aesWrappedDek, 0, combined, ciphertext.length, aesWrappedDek.length);
58+
return combined;
59+
}
60+
}

sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA
233233
keyAccess.wrappedKey = ecKeyWrappedKeyInfo.wrappedKey;
234234
keyAccess.ephemeralPublicKey = ecKeyWrappedKeyInfo.publicKey;
235235
keyAccess.keyType = kECWrapped;
236+
} else if (keyType.isMlkem()) {
237+
keyAccess.wrappedKey = createMLKEMWrappedKey(kasInfo, symKey);
238+
keyAccess.keyType = kWrapped;
236239
} else {
237240
keyAccess.wrappedKey = createRSAWrappedKey(kasInfo, symKey);
238241
keyAccess.keyType = kWrapped;
@@ -264,6 +267,11 @@ private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) {
264267
byte[] wrappedKey = asymEncrypt.encrypt(symKey);
265268
return Base64.getEncoder().encodeToString(wrappedKey);
266269
}
270+
271+
private String createMLKEMWrappedKey(Config.KASInfo kasInfo, byte[] symKey) {
272+
MLKEMEncryption mlkem = new MLKEMEncryption(kasInfo.PublicKey);
273+
return Base64.getEncoder().encodeToString(mlkem.encapsulateAndWrap(symKey));
274+
}
267275
}
268276

269277
private static final Base64.Decoder decoder = Base64.getDecoder();

xtest/sdk/java/cli.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
# Cross-SDK test CLI helper for the OpenTDF Java SDK.
3+
# Called by the xtest harness to check feature support and run encrypt/decrypt ops.
4+
set -euo pipefail
5+
6+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
7+
JAR="${REPO_ROOT}/cmdline/target/cmdline.jar"
8+
9+
_jar_help() {
10+
java -jar "${JAR}" "$@" --help 2>&1 || true
11+
}
12+
13+
case "${1:-}" in
14+
supports)
15+
feature="${2:-}"
16+
case "$feature" in
17+
mechanism-mlkem)
18+
# mlkem:768 is a valid --encap-key-type value; picocli lists it in the
19+
# encrypt help as a COMPLETION-CANDIDATE from KeyType.MLKEM768Key.toString()
20+
_jar_help encrypt | grep -q "mlkem:768"
21+
;;
22+
*)
23+
exit 1
24+
;;
25+
esac
26+
;;
27+
encrypt)
28+
shift
29+
java -jar "${JAR}" encrypt "$@"
30+
;;
31+
decrypt)
32+
shift
33+
java -jar "${JAR}" decrypt "$@"
34+
;;
35+
*)
36+
echo "usage: $0 {supports <feature>|encrypt ...|decrypt ...}" >&2
37+
exit 1
38+
;;
39+
esac

0 commit comments

Comments
 (0)