From 3bdffa87b40ccae6230a4b95d7eaa52ba8baa24a Mon Sep 17 00:00:00 2001 From: sujan kota Date: Sun, 23 Feb 2025 08:15:51 -0500 Subject: [PATCH 1/5] feat(sdk): EC-wrapped key support for ZTDF --- examples/pom.xml | 5 + .../io/opentdf/platform/DecryptExample.java | 37 +++-- .../io/opentdf/platform/EncryptExample.java | 39 +++-- .../java/io/opentdf/platform/sdk/Config.java | 13 +- .../io/opentdf/platform/sdk/CryptoUtils.java | 25 ++++ .../io/opentdf/platform/sdk/KASClient.java | 39 ++++- .../java/io/opentdf/platform/sdk/KeyType.java | 41 ++++++ .../io/opentdf/platform/sdk/Manifest.java | 12 +- .../java/io/opentdf/platform/sdk/SDK.java | 2 +- .../java/io/opentdf/platform/sdk/TDF.java | 87 ++++++++--- .../platform/sdk/nanotdf/ECKeyPair.java | 12 ++ .../opentdf/platform/sdk/KASClientTest.java | 2 +- .../io/opentdf/platform/sdk/NanoTDFTest.java | 2 +- .../opentdf/platform/sdk/SDKBuilderTest.java | 2 +- .../io/opentdf/platform/sdk/TDFE2ETest.java | 44 ++++-- .../java/io/opentdf/platform/sdk/TDFTest.java | 135 ++++++++++++------ 16 files changed, 388 insertions(+), 109 deletions(-) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java diff --git a/examples/pom.xml b/examples/pom.xml index 92176b16..e782e24e 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -61,6 +61,11 @@ org.apache.logging.log4j log4j-core + + commons-cli + commons-cli + 1.4 + org.apache.logging.log4j log4j-api diff --git a/examples/src/main/java/io/opentdf/platform/DecryptExample.java b/examples/src/main/java/io/opentdf/platform/DecryptExample.java index 693192ca..92698ab2 100644 --- a/examples/src/main/java/io/opentdf/platform/DecryptExample.java +++ b/examples/src/main/java/io/opentdf/platform/DecryptExample.java @@ -1,13 +1,12 @@ package io.opentdf.platform; + import io.opentdf.platform.sdk.*; import java.nio.file.StandardOpenOption; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.Paths; - import com.nimbusds.jose.JOSEException; import java.io.IOException; -import java.util.concurrent.ExecutionException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -15,14 +14,33 @@ import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import org.apache.commons.cli.*; import org.apache.commons.codec.DecoderException; - public class DecryptExample { public static void main(String[] args) throws IOException, - InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, - BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC, - JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { + InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, + BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC, + JOSEException, ParseException, NoSuchAlgorithmException, DecoderException, org.apache.commons.cli.ParseException { + + // Create Options object + Options options = new Options(); + + // Add rewrap encapsulation algorithm option + options.addOption(Option.builder("A") + .longOpt("rewrap-encapsulation-algorithm") + .hasArg() + .desc("Key wrap response algorithm algorithm:parameters") + .build()); + + // Parse command line arguments + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(options, args); + + // Get the rewrap encapsulation algorithm + String rewrapEncapsulationAlgorithm = cmd.getOptionValue("rewrap-encapsulation-algorithm", "rsa:2048"); + var sessionKeyType = KeyType.fromString(rewrapEncapsulationAlgorithm.toLowerCase()); + String clientId = "opentdf"; String clientSecret = "secret"; @@ -35,8 +53,11 @@ public static void main(String[] args) throws IOException, Path path = Paths.get("my.ciphertext"); try (var in = FileChannel.open(path, StandardOpenOption.READ)) { - var reader = new TDF().loadTDF(in, sdk.getServices().kas()); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), Config.newTDFReaderConfig(Config.WithSessionKeyType(sessionKeyType))); reader.readPayload(System.out); } + + // Print the rewrap encapsulation algorithm + System.out.println("Rewrap Encapsulation Algorithm: " + rewrapEncapsulationAlgorithm); } -} +} \ No newline at end of file diff --git a/examples/src/main/java/io/opentdf/platform/EncryptExample.java b/examples/src/main/java/io/opentdf/platform/EncryptExample.java index 6dcccb0b..f9ac0176 100644 --- a/examples/src/main/java/io/opentdf/platform/EncryptExample.java +++ b/examples/src/main/java/io/opentdf/platform/EncryptExample.java @@ -1,18 +1,35 @@ package io.opentdf.platform; + import io.opentdf.platform.sdk.*; import java.io.ByteArrayInputStream; -import java.io.BufferedOutputStream; import java.nio.charset.StandardCharsets; import java.io.FileOutputStream; - import com.nimbusds.jose.JOSEException; +import org.apache.commons.cli.*; import org.apache.commons.codec.DecoderException; - import java.io.IOException; import java.util.concurrent.ExecutionException; public class EncryptExample { - public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException, DecoderException { + public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException, + InterruptedException, ExecutionException, DecoderException, ParseException { + // Create Options object + Options options = new Options(); + + // Add key encapsulation algorithm option + options.addOption(Option.builder("A") + .longOpt("key-encapsulation-algorithm") + .hasArg() + .desc("Key wrap algorithm algorithm:parameters") + .build()); + + // Parse command line arguments + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(options, args); + + // Get the key encapsulation algorithm + String keyEncapsulationAlgorithm = cmd.getOptionValue("key-encapsulation-algorithm", "rsa:2048"); + String clientId = "opentdf"; String clientSecret = "secret"; String platformEndpoint = "localhost:8080"; @@ -25,17 +42,19 @@ public static void main(String[] args) throws IOException, JOSEException, AutoCo var kasInfo = new Config.KASInfo(); kasInfo.URL = "http://localhost:8080/kas"; - var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo), Config.withDataAttributes("https://example.com/attr/color/value/red")); - + var wrappingKeyType = KeyType.fromString(keyEncapsulationAlgorithm.toLowerCase()); + var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo), + Config.withDataAttributes("https://example.com/attr/color/value/red"), + Config.WithWrappingKeyAlg(wrappingKeyType)); String str = "Hello, World!"; - + // Convert String to InputStream var in = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); FileOutputStream fos = new FileOutputStream("my.ciphertext"); new TDF().createTDF(in, fos, tdfConfig, - sdk.getServices().kas(), - sdk.getServices().attributes()); + sdk.getServices().kas(), + sdk.getServices().attributes()); } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index a8b58c4b..2b1e0611 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -7,10 +7,8 @@ import io.opentdf.platform.sdk.nanotdf.SymmetricAndPayloadConfig; import io.opentdf.platform.policy.Value; -import org.bouncycastle.oer.its.ieee1609dot2.HeaderInfo; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; /** @@ -99,12 +97,14 @@ public static class TDFReaderConfig { // Optional Map of Assertion Verification Keys AssertionVerificationKeys assertionVerificationKeys = new AssertionVerificationKeys(); boolean disableAssertionVerification; + KeyType sessionKeyType;; } @SafeVarargs public static TDFReaderConfig newTDFReaderConfig(Consumer... options) { TDFReaderConfig config = new TDFReaderConfig(); config.disableAssertionVerification = false; + config.sessionKeyType = KeyType.RSA2048Key; for (Consumer option : options) { option.accept(config); } @@ -120,6 +120,9 @@ public static Consumer withDisableAssertionVerification(boolean return (TDFReaderConfig config) -> config.disableAssertionVerification = disable; } + public static Consumer WithSessionKeyType(KeyType keyType) { + return (TDFReaderConfig config) -> config.sessionKeyType = keyType; + } public static class TDFConfig { public Boolean autoconfigure; public int defaultSegmentSize; @@ -136,6 +139,7 @@ public static class TDFConfig { public List assertionConfigList; public String mimeType; public List splitPlan; + public KeyType wrappingKeyType; public TDFConfig() { this.autoconfigure = true; @@ -149,6 +153,7 @@ public TDFConfig() { this.assertionConfigList = new ArrayList<>(); this.mimeType = DEFAULT_MIME_TYPE; this.splitPlan = new ArrayList<>(); + this.wrappingKeyType = KeyType.RSA2048Key; } } @@ -246,6 +251,10 @@ public static Consumer withAutoconfigure(boolean enable) { }; } + public static Consumer WithWrappingKeyAlg(KeyType keyType) { + return (TDFConfig config) -> config.wrappingKeyType = keyType; + } + // public static Consumer withDisableEncryption() { // return (TDFConfig config) -> config.enableEncryption = false; // } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java index 404637df..e7f8c5c3 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java @@ -3,6 +3,7 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.*; +import java.security.spec.ECGenParameterSpec; import java.util.Base64; /** @@ -39,6 +40,30 @@ public static KeyPair generateRSAKeypair() { return kpg.generateKeyPair(); } + public static KeyPair generateECKeypair(String curveName) { + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName); + kpg.initialize(ecSpec); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new SDKException("error creating EC keypair", e); + } + return kpg.generateKeyPair(); + } + + public static String getPublicKeyPEM(PublicKey publicKey) { + return "-----BEGIN PUBLIC KEY-----\r\n" + + Base64.getMimeEncoder().encodeToString(publicKey.getEncoded()) + + "\r\n-----END PUBLIC KEY-----"; + } + + public static String getPrivateKeyPEM(PrivateKey privateKey) { + return "-----BEGIN PRIVATE KEY-----\r\n" + + Base64.getMimeEncoder().encodeToString(privateKey.getEncoded()) + + "\r\n-----END PRIVATE KEY-----"; + } + public static String getRSAPublicKeyPEM(PublicKey publicKey) { if (!"RSA".equals(publicKey.getAlgorithm())) { throw new IllegalArgumentException("can't get public key PEM for algorithm [" + publicKey.getAlgorithm() + "]"); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index 806d48d0..17a3093a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -17,10 +17,12 @@ import io.opentdf.platform.kas.RewrapRequest; import io.opentdf.platform.kas.RewrapResponse; import io.opentdf.platform.sdk.Config.KASInfo; +import io.opentdf.platform.sdk.nanotdf.CryptoKeyPair; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import io.opentdf.platform.sdk.TDF.KasBadRequestException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.net.MalformedURLException; @@ -32,6 +34,7 @@ import java.util.HashMap; import java.util.function.Function; +import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; import static java.lang.String.format; /** @@ -45,7 +48,7 @@ public class KASClient implements SDK.KAS { private final RSASSASigner signer; private final AsymDecryption decryptor; private final String publicKeyPEM; - + private CryptoKeyPair keyPair; private KASKeyCache kasKeyCache; /*** @@ -86,8 +89,9 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { if (cachedValue != null) { return cachedValue; } - PublicKeyResponse resp = getStub(kasInfo.URL).publicKey(PublicKeyRequest.getDefaultInstance()); - + var resp = getStub(kasInfo.URL) + .publicKey( + PublicKeyRequest.newBuilder().setAlgorithm(kasInfo.Algorithm).build()); var kiCopy = new Config.KASInfo(); kiCopy.KID = resp.getKid(); kiCopy.PublicKey = resp.getPublicKey(); @@ -161,11 +165,19 @@ static class NanoTDFRewrapRequestBody { private static final Gson gson = new Gson(); @Override - public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) { + public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + ECKeyPair ecKeyPair = null; RewrapRequestBody body = new RewrapRequestBody(); body.policy = policy; body.clientPublicKey = publicKeyPEM; body.keyAccess = keyAccess; + + if (sessionKeyType != KeyType.RSA2048Key) { + var curveName =sessionKeyType.getCurveName(); + ecKeyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH); + body.clientPublicKey = ecKeyPair.publicKeyInPEMFormat(); + } + var requestBody = gson.toJson(body); var claims = new JWTClaimsSet.Builder() @@ -190,7 +202,24 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) { try { response = getStub(keyAccess.url).rewrap(request); var wrappedKey = response.getEntityWrappedKey().toByteArray(); - return decryptor.decrypt(wrappedKey); + if (sessionKeyType != KeyType.RSA2048Key) { + + if (ecKeyPair == null) { + throw new SDKException("ECKeyPair is null. Unable to proceed with the unwrap operation."); + } + + var kasEphemeralPublicKey = response.getSessionPublicKey(); + var publicKey = ECKeyPair.publicKeyFromPem(kasEphemeralPublicKey); + byte[] symKey = ECKeyPair.computeECDHKey(publicKey, ecKeyPair.getPrivateKey()); + + var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); + + AesGcm gcm = new AesGcm(sessionKey); + AesGcm.Encrypted encrypted = new AesGcm.Encrypted(wrappedKey); + return gcm.decrypt(encrypted); + } else { + return decryptor.decrypt(wrappedKey); + } } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) { // 400 Bad Request diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java new file mode 100644 index 00000000..4f1a94f3 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -0,0 +1,41 @@ +package io.opentdf.platform.sdk; + +public enum KeyType { + RSA2048Key("rsa:2048"), + EC256Key("ec:secp256r1"), + EC384Key("ec:secp384r1"), + EC521Key("ec:secp521r1"); + + private final String keyType; + + KeyType(String keyType) { + this.keyType = keyType; + } + + @Override + public String toString() { + return keyType; + } + + public String getCurveName() { + switch (this) { + case EC256Key: + return "secp256r1"; + case EC384Key: + return "secp384r1"; + case EC521Key: + return "secp521r1"; + default: + throw new IllegalArgumentException("Unsupported key type: " + this); + } + } + + public static KeyType fromString(String keyType) { + for (KeyType type : KeyType.values()) { + if (type.keyType.equalsIgnoreCase(keyType)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant for key type: " + keyType); + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java index 253c6207..32c50b19 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -197,25 +197,25 @@ static public class KeyAccess { public String kid; public String sid; public String schemaVersion; + public String ephemeralPublicKey; @Override public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; KeyAccess keyAccess = (KeyAccess) o; return Objects.equals(keyType, keyAccess.keyType) && Objects.equals(url, keyAccess.url) && Objects.equals(protocol, keyAccess.protocol) && Objects.equals(wrappedKey, keyAccess.wrappedKey) && Objects.equals(policyBinding, keyAccess.policyBinding) && Objects.equals(encryptedMetadata, keyAccess.encryptedMetadata) && Objects.equals(kid, keyAccess.kid) - && Objects.equals(schemaVersion, keyAccess.schemaVersion); + && Objects.equals(schemaVersion, keyAccess.schemaVersion) + && Objects.equals(ephemeralPublicKey, keyAccess.ephemeralPublicKey); } @Override public int hashCode() { - return Objects.hash(keyType, url, protocol, wrappedKey, policyBinding, encryptedMetadata, kid, schemaVersion); + return Objects.hash(keyType, url, protocol, wrappedKey, policyBinding, encryptedMetadata, kid, schemaVersion, ephemeralPublicKey); } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java index 14affa4c..20928030 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -53,7 +53,7 @@ public interface KAS extends AutoCloseable { Config.KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve); - byte[] unwrap(Manifest.KeyAccess keyAccess, String policy); + byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType); byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL); 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 73e7fbe3..94ba619d 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -10,8 +10,10 @@ import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; import io.opentdf.platform.sdk.Config.KASInfo; +import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.jce.interfaces.ECPublicKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,9 +37,10 @@ */ public class TDF { + public static final byte[] GLOBAL_KEY_SALT = "salt".getBytes(StandardCharsets.UTF_8); private static final String EMPTY_SPLIT_ID = ""; private static final String TDF_VERSION = "4.3.0"; - private static final String KEY_ACCESS_SECHMA_VERSION = "4.3.0"; + private static final String KEY_ACCESS_SECHMA_VERSION = "1.0"; private final long maximumSize; /** @@ -63,6 +66,7 @@ public TDF() { private static final int GCM_KEY_SIZE = 32; private static final String kSplitKeyType = "split"; private static final String kWrapped = "wrapped"; + private static final String kECWrapped = "ec-wrapped"; private static final String kKasProtocol = "kas"; private static final int kGcmIvSize = 12; private static final int kAesBlockSize = 16; @@ -168,6 +172,11 @@ public static class EncryptedMetadata { private String iv; } + public static class ECKeyWrappedKeyInfo { + private String publicKey; + private String wrappedKey; + } + public static class TDFObject { private Manifest manifest; private long size; @@ -244,7 +253,7 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas); var getKI = new Config.KASInfo(); getKI.URL = splitInfo.kas; - getKI.Algorithm = "rsa:2048"; + getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); getKI = kas.getPublicKey(getKI); latestKASInfo.put(splitInfo.kas, getKI); ki = getKI; @@ -292,21 +301,7 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { throw new KasPublicKeyMissing("Kas public key is missing in kas information list"); } - // Wrap the key with kas public key - AsymEncryption asymmetricEncrypt = new AsymEncryption(kasInfo.PublicKey); - byte[] wrappedKey = asymmetricEncrypt.encrypt(symKey); - - Manifest.KeyAccess keyAccess = new Manifest.KeyAccess(); - keyAccess.keyType = kWrapped; - keyAccess.url = kasInfo.URL; - keyAccess.kid = kasInfo.KID; - keyAccess.protocol = kKasProtocol; - keyAccess.policyBinding = policyBinding; - keyAccess.wrappedKey = encoder.encodeToString(wrappedKey); - keyAccess.encryptedMetadata = encryptedMetadata; - keyAccess.sid = splitID; - keyAccess.schemaVersion = KEY_ACCESS_SECHMA_VERSION; - + var keyAccess = createKeyAccess(tdfConfig, kasInfo, symKey, policyBinding, encryptedMetadata, splitID); manifest.encryptionInformation.keyAccessObj.add(keyAccess); } } @@ -323,8 +318,56 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { this.aesGcm = new AesGcm(this.payloadKey); } + + private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, byte[] symKey, Manifest.PolicyBinding policyBinding, String encryptedMetadata, String splitID) { + Manifest.KeyAccess keyAccess = new Manifest.KeyAccess(); + keyAccess.keyType = kWrapped; + keyAccess.url = kasInfo.URL; + keyAccess.kid = kasInfo.KID; + keyAccess.protocol = kKasProtocol; + keyAccess.policyBinding = policyBinding; + keyAccess.encryptedMetadata = encryptedMetadata; + keyAccess.sid = splitID; + keyAccess.schemaVersion = KEY_ACCESS_SECHMA_VERSION; + + if (tdfConfig.wrappingKeyType != KeyType.RSA2048Key) { + var ecKeyWrappedKeyInfo =createECWrappedKey(tdfConfig, kasInfo, symKey); + keyAccess.wrappedKey = ecKeyWrappedKeyInfo.wrappedKey; + keyAccess.ephemeralPublicKey = ecKeyWrappedKeyInfo.publicKey; + keyAccess.keyType = kECWrapped; + } else { + keyAccess.wrappedKey = createRSAWrappedKey(kasInfo, symKey); + keyAccess.keyType = kWrapped; + } + return keyAccess; + } + + private ECKeyWrappedKeyInfo createECWrappedKey(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, byte[] symKey) { + var curveName = tdfConfig.wrappingKeyType.getCurveName(); + var keyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH); + + ECPublicKey kasPubKey = ECKeyPair.publicKeyFromPem(kasInfo.PublicKey); + byte[] symmetricKey = ECKeyPair.computeECDHKey(kasPubKey, keyPair.getPrivateKey()); + + var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symmetricKey); + + AesGcm gcm = new AesGcm(sessionKey); + AesGcm.Encrypted wrappedKey = gcm.encrypt(symKey); + + ECKeyWrappedKeyInfo wrappedKeyInfo = new ECKeyWrappedKeyInfo(); + wrappedKeyInfo.publicKey = keyPair.publicKeyInPEMFormat(); + wrappedKeyInfo.wrappedKey = Base64.getEncoder().encodeToString(wrappedKey.asBytes()); + return wrappedKeyInfo; + } + + private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) { + AsymEncryption asymEncrypt = new AsymEncryption(kasInfo.PublicKey); + byte[] wrappedKey = asymEncrypt.encrypt(symKey); + return Base64.getEncoder().encodeToString(wrappedKey); + } } + private static final Base64.Decoder decoder = Base64.getDecoder(); public static class Reader { @@ -419,8 +462,8 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte } public TDFObject createTDF(InputStream payload, - OutputStream outputStream, - Config.TDFConfig tdfConfig, SDK.KAS kas, AttributesServiceFutureStub attrService) + OutputStream outputStream, + Config.TDFConfig tdfConfig, SDK.KAS kas, AttributesServiceFutureStub attrService) throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException, DecoderException { if (tdfConfig.autoconfigure) { @@ -441,7 +484,7 @@ public TDFObject createTDF(InputStream payload, if (tdfConfig.splitPlan == null) { throw new AutoConfigureException("Failed to generate Split Plan"); // Replace with appropriate error - // handling + // handling } } @@ -557,7 +600,7 @@ public TDFObject createTDF(InputStream payload, } tdfObject.manifest.assertions = signedAssertions; - String manifestAsStr = Manifest.toJson(tdfObject.manifest); + String manifestAsStr = gson.toJson(tdfObject.manifest); tdfWriter.appendManifest(manifestAsStr); tdfObject.size = tdfWriter.finish(); @@ -616,7 +659,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, } knownSplits.add(ss.splitID); try { - unwrappedKey = kas.unwrap(keyAccess, manifest.encryptionInformation.policy); + unwrappedKey = kas.unwrap(keyAccess, manifest.encryptionInformation.policy, tdfReaderConfig.sessionKeyType); } catch (Exception e) { skippedSplits.put(ss, e); continue; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java index 72641e68..9cede0b2 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java @@ -1,5 +1,6 @@ package io.opentdf.platform.sdk.nanotdf; +import io.opentdf.platform.sdk.KeyType; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.cert.X509CertificateHolder; @@ -145,6 +146,17 @@ public String privateKeyInPEMFormat() { return writer.toString(); } + public KeyType getKeyType() { + if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP256R1.toString())) { + return KeyType.EC256Key; + } else if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP384R1.toString())) { + return KeyType.EC384Key; + } else if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP521R1.toString())) { + return KeyType.EC521Key; + } + return null; + } + public int keySize() { return this.keyPair.getPrivate().getEncoded().length * 8; } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java index f44bc564..496f4708 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java @@ -165,7 +165,7 @@ public void rewrap(RewrapRequest request, StreamObserver respons var serverWrappedKey = new AsymEncryption(serverKeypair.getPublic()).encrypt(plaintextKey); keyAccess.wrappedKey = Base64.getEncoder().encodeToString(serverWrappedKey); - rewrapResponse = kas.unwrap(keyAccess, "the policy"); + rewrapResponse = kas.unwrap(keyAccess, "the policy", KeyType.RSA2048Key); } assertThat(rewrapResponse).containsExactly(plaintextKey); } finally { diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java index cd4c056d..0c8203ae 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java @@ -61,7 +61,7 @@ public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) } @Override - public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) { + public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { int index = Integer.parseInt(keyAccess.url); var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java index 0a85a181..1f1af97e 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java @@ -300,7 +300,7 @@ public ServerCall.Listener interceptCall(ServerCall tdfConfigPairs = List.of( + new TDFConfigPair( + Config.newTDFConfig(Config.withKasInformation(kasInfo)), + Config.newTDFReaderConfig() + ), + new TDFConfigPair( + Config.newTDFConfig(Config.withKasInformation(kasInfo), Config.WithWrappingKeyAlg(KeyType.EC256Key)), + Config.newTDFReaderConfig(Config.WithSessionKeyType(KeyType.EC256Key)) + ) + ); + + for (TDFConfigPair configPair : tdfConfigPairs) { + String plainText = "text"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config, sdk.kas(), sdk.attributes()); + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig, sdk.kas(), sdk.attributes()); - var unwrappedData = new java.io.ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.kas()); - reader.readPayload(unwrappedData); + var unwrappedData = new java.io.ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.kas(), configPair.tdfReaderConfig); + reader.readPayload(unwrappedData); - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)).isEqualTo("text"); + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)).isEqualTo("text"); + } } @Test @Disabled("this needs the backend services running to work") diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 87090bf5..4814ab23 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -3,8 +3,10 @@ import com.nimbusds.jose.JOSEException; import io.opentdf.platform.sdk.Config.KASInfo; import io.opentdf.platform.sdk.TDF.Reader; +import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.bouncycastle.jce.interfaces.ECPrivateKey; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -18,10 +20,12 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -36,18 +40,33 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { int index = Integer.parseInt(kasInfo.URL); var kiCopy = new Config.KASInfo(); kiCopy.KID = "r1"; - kiCopy.PublicKey = CryptoUtils.getRSAPublicKeyPEM(keypairs.get(index).getPublic()); + kiCopy.PublicKey = CryptoUtils.getPublicKeyPEM(keypairs.get(index).getPublic()); kiCopy.URL = kasInfo.URL; return kiCopy; } @Override - public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) { - int index = Integer.parseInt(keyAccess.url); - var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); - var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); + public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + try { - return decryptor.decrypt(bytes); + int index = Integer.parseInt(keyAccess.url); + var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); + if (sessionKeyType != KeyType.RSA2048Key) { + var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); + var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); + var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; + var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); + byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey); + + var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); + + AesGcm gcm = new AesGcm(sessionKey); + AesGcm.Encrypted encrypted = new AesGcm.Encrypted(bytes); + return gcm.decrypt(encrypted); + } else { + var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); + return decryptor.decrypt(bytes); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -74,17 +93,32 @@ public KASKeyCache getKeyCache() { @BeforeAll static void createKeypairs() { for (int i = 0; i < 1 + new Random().nextInt(5); i++) { - keypairs.add(CryptoUtils.generateRSAKeypair()); + if (i % 2 == 0) { + keypairs.add(CryptoUtils.generateRSAKeypair()); + } else { + keypairs.add(CryptoUtils.generateECKeypair(KeyType.EC256Key.getCurveName())); + } } } @Test void testSimpleTDFEncryptAndDecrypt() throws Exception { + + class TDFConfigPair { + public final Config.TDFConfig tdfConfig; + public final Config.TDFReaderConfig tdfReaderConfig; + + public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReaderConfig) { + this.tdfConfig = tdfConfig; + this.tdfReaderConfig = tdfReaderConfig; + } + } + SecureRandom secureRandom = new SecureRandom(); byte[] key = new byte[32]; secureRandom.nextBytes(key); - var assertion1 = new AssertionConfig(); + var assertion1 = new AssertionConfig(); assertion1.id = "assertion1"; assertion1.type = AssertionConfig.Type.BaseAssertion; assertion1.scope = AssertionConfig.Scope.TrustedDataObj; @@ -95,43 +129,62 @@ void testSimpleTDFEncryptAndDecrypt() throws Exception { assertion1.statement.value = "ICAgIDxlZGoOkVkaD4="; assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), - Config.withMetaData("here is some metadata"), - Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), - Config.withAssertionConfig(assertion1)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null); - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - var unwrappedData = new ByteArrayOutputStream(); - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - readerConfig); - assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); - - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); - - var policyObject = reader.readPolicyObject(); - assertThat(policyObject).isNotNull(); - assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())).asList() - .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"); + // odd - RSA + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = Integer.toString(0); + + // even - EC + var ecKasInfo = new Config.KASInfo(); + ecKasInfo.URL =Integer.toString(1); + + List tdfConfigPairs = List.of( + new TDFConfigPair( + Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(rsaKasInfo), + Config.withMetaData("here is some metadata"), + Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys)) + ), + new TDFConfigPair( + Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(ecKasInfo), + Config.withMetaData("here is some metadata"), + Config.WithWrappingKeyAlg(KeyType.EC256Key), + Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys), + Config.WithSessionKeyType(KeyType.EC256Key)) + ) + ); + + for (TDFConfigPair configPair : tdfConfigPairs) { + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig, kas, null); + + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, + configPair.tdfReaderConfig); + assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); + + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); + + var policyObject = reader.readPolicyObject(); + assertThat(policyObject).isNotNull(); + assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())).asList() + .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"); + } } @Test From 26bdafd89d4cb4d3b0db464e78305c44a73c3bad Mon Sep 17 00:00:00 2001 From: sujan kota Date: Sun, 23 Feb 2025 08:23:32 -0500 Subject: [PATCH 2/5] Fix the build --- sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index 17a3093a..4f6bf5cf 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -17,7 +17,6 @@ import io.opentdf.platform.kas.RewrapRequest; import io.opentdf.platform.kas.RewrapResponse; import io.opentdf.platform.sdk.Config.KASInfo; -import io.opentdf.platform.sdk.nanotdf.CryptoKeyPair; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import io.opentdf.platform.sdk.TDF.KasBadRequestException; @@ -48,8 +47,7 @@ public class KASClient implements SDK.KAS { private final RSASSASigner signer; private final AsymDecryption decryptor; private final String publicKeyPEM; - private CryptoKeyPair keyPair; - private KASKeyCache kasKeyCache; + private KASKeyCache kasKeyCache; /*** * A client that communicates with KAS From 7d8736ab71f1823cf1d8e12f020009798bd89777 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Sun, 23 Feb 2025 13:46:46 -0500 Subject: [PATCH 3/5] Fix the build --- .../io/opentdf/platform/sdk/KASClient.java | 12 ++-- .../java/io/opentdf/platform/sdk/TDF.java | 2 +- .../java/io/opentdf/platform/sdk/TDFTest.java | 64 +++++++++++-------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index 4f6bf5cf..d4647060 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -47,7 +47,7 @@ public class KASClient implements SDK.KAS { private final RSASSASigner signer; private final AsymDecryption decryptor; private final String publicKeyPEM; - private KASKeyCache kasKeyCache; + private KASKeyCache kasKeyCache; /*** * A client that communicates with KAS @@ -87,9 +87,13 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { if (cachedValue != null) { return cachedValue; } - var resp = getStub(kasInfo.URL) - .publicKey( - PublicKeyRequest.newBuilder().setAlgorithm(kasInfo.Algorithm).build()); + + PublicKeyRequest request = (kasInfo.Algorithm == null || kasInfo.Algorithm.isEmpty()) + ? PublicKeyRequest.getDefaultInstance() + : PublicKeyRequest.newBuilder().setAlgorithm(kasInfo.Algorithm).build(); + + PublicKeyResponse resp = getStub(kasInfo.URL).publicKey(request); + var kiCopy = new Config.KASInfo(); kiCopy.KID = resp.getKid(); kiCopy.PublicKey = resp.getPublicKey(); 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 94ba619d..7dabe888 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -627,7 +627,7 @@ public List defaultKases(TDFConfig config) { public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas) throws DecoderException, IOException, ParseException, NoSuchAlgorithmException, JOSEException { - return loadTDF(tdf, kas, new Config.TDFReaderConfig()); + return loadTDF(tdf, kas, Config.newTDFReaderConfig()); } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 4814ab23..2841f852 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import java.util.stream.Collectors; import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; @@ -133,24 +134,16 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - // odd - RSA - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = Integer.toString(0); - - // even - EC - var ecKasInfo = new Config.KASInfo(); - ecKasInfo.URL =Integer.toString(1); - List tdfConfigPairs = List.of( new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(rsaKasInfo), + Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getRSAKASInfos()), Config.withMetaData("here is some metadata"), Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), Config.withAssertionConfig(assertion1)), Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys)) ), new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(ecKasInfo), + Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getECKASInfos()), Config.withMetaData("here is some metadata"), Config.WithWrappingKeyAlg(KeyType.EC256Key), Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), @@ -203,9 +196,12 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPrivate()); + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = Integer.toString(0); + Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(rsaKasInfo), Config.withAssertionConfig(assertionConfig)); String plainText = "this is extremely sensitive stuff!!!"; @@ -249,7 +245,7 @@ void testWithAssertionVerificationDisabled() throws Exception { Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(getRSAKASInfos()), Config.withAssertionConfig(assertionConfig)); String plainText = "this is extremely sensitive stuff!!!"; @@ -266,7 +262,7 @@ void testWithAssertionVerificationDisabled() throws Exception { var unwrappedData = new ByteArrayOutputStream(); assertThrows(JOSEException.class, () -> { tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - new Config.TDFReaderConfig()); + Config.newTDFReaderConfig()); }); // try with assertion verification disabled and not passing the assertion verification keys @@ -304,9 +300,12 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { assertionConfig2.statement.schema = "urn:nato:stanag:5636:A:1:elements:json"; assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = Integer.toString(0); + Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(rsaKasInfo), Config.withAssertionConfig(assertionConfig1, assertionConfig2)); String plainText = "this is extremely sensitive stuff!!!"; @@ -318,7 +317,7 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { var unwrappedData = new ByteArrayOutputStream(); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), - kas, new Config.TDFReaderConfig()); + kas, Config.newTDFReaderConfig()); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -365,9 +364,12 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = Integer.toString(0); + Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(rsaKasInfo), Config.withAssertionConfig(assertionConfig1)); String plainText = "this is extremely sensitive stuff!!!"; @@ -402,7 +404,7 @@ public void testCreatingTDFWithMultipleSegments() throws Exception { Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(getRSAKASInfos()), Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); // data should be large enough to have multiple complete and a partial segment @@ -460,7 +462,7 @@ public void write(byte[] b, int off, int len) { var tdf = new TDF(maxSize); var tdfConfig = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(getRSAKASInfos()), Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); assertThrows(TDF.DataSizeNotSupported.class, () -> tdf.createTDF(is, os, tdfConfig, kas, null), @@ -476,7 +478,7 @@ public void testCreateTDFWithMimeType() throws Exception { Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), - Config.withKasInformation(getKASInfos()), + Config.withKasInformation(getRSAKASInfos()), Config.withMimeType(mimeType)); String plainText = "this is extremely sensitive stuff!!!"; @@ -491,14 +493,26 @@ public void testCreateTDFWithMimeType() throws Exception { } @Nonnull - private static Config.KASInfo[] getKASInfos() { - var kasInfos = new ArrayList<>(); + private static Config.KASInfo[] getKASInfos(Predicate filter) { + var kasInfos = new ArrayList(); for (int i = 0; i < keypairs.size(); i++) { - var kasInfo = new Config.KASInfo(); - kasInfo.URL = Integer.toString(i); - kasInfo.PublicKey = null; - kasInfos.add(kasInfo); + if (filter.test(i)) { + var kasInfo = new Config.KASInfo(); + kasInfo.URL = Integer.toString(i); + kasInfo.PublicKey = null; + kasInfos.add(kasInfo); + } } return kasInfos.toArray(Config.KASInfo[]::new); } + + @Nonnull + private static Config.KASInfo[] getRSAKASInfos() { + return getKASInfos(i -> i % 2 == 0); + } + + @Nonnull + private static Config.KASInfo[] getECKASInfos() { + return getKASInfos(i -> i % 2 != 0); + } } From 4e7df1b1bba48425d106be06b0a8692bf4c9cac5 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Sun, 23 Feb 2025 13:54:59 -0500 Subject: [PATCH 4/5] fix the build --- sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 2841f852..4d72d637 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -93,7 +93,7 @@ public KASKeyCache getKeyCache() { @BeforeAll static void createKeypairs() { - for (int i = 0; i < 1 + new Random().nextInt(5); i++) { + for (int i = 0; i < 2 + new Random().nextInt(5); i++) { if (i % 2 == 0) { keypairs.add(CryptoUtils.generateRSAKeypair()); } else { From f00127d62c167743b401b4d8310db4ba6b78f990 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Wed, 26 Feb 2025 13:39:47 -0500 Subject: [PATCH 5/5] Addressing code review comments --- .../java/io/opentdf/platform/sdk/Config.java | 2 +- .../io/opentdf/platform/sdk/KASClient.java | 31 ++++++++++--------- .../java/io/opentdf/platform/sdk/KeyType.java | 4 +++ .../java/io/opentdf/platform/sdk/SDK.java | 3 +- .../java/io/opentdf/platform/sdk/TDF.java | 4 +-- .../platform/sdk/nanotdf/ECKeyPair.java | 27 +++++++--------- .../java/io/opentdf/platform/sdk/TDFTest.java | 2 +- 7 files changed, 38 insertions(+), 35 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 2b1e0611..3ec227f4 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -97,7 +97,7 @@ public static class TDFReaderConfig { // Optional Map of Assertion Verification Keys AssertionVerificationKeys assertionVerificationKeys = new AssertionVerificationKeys(); boolean disableAssertionVerification; - KeyType sessionKeyType;; + KeyType sessionKeyType; } @SafeVarargs diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index d4647060..def107fc 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -45,8 +45,8 @@ public class KASClient implements SDK.KAS { private final Function channelFactory; private final RSASSASigner signer; - private final AsymDecryption decryptor; - private final String publicKeyPEM; + private AsymDecryption decryptor; + private String clientPublicKey; private KASKeyCache kasKeyCache; /*** @@ -63,10 +63,6 @@ public KASClient(Function channelFactory, RSAKey dpopKey } catch (JOSEException e) { throw new SDKException("error creating dpop signer", e); } - var encryptionKeypair = CryptoUtils.generateRSAKeypair(); - decryptor = new AsymDecryption(encryptionKeypair.getPrivate()); - publicKeyPEM = CryptoUtils.getRSAPublicKeyPEM(encryptionKeypair.getPublic()); - this.kasKeyCache = new KASKeyCache(); } @@ -169,19 +165,26 @@ static class NanoTDFRewrapRequestBody { @Override public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { ECKeyPair ecKeyPair = null; + + if (sessionKeyType.isEc()) { + var curveName = sessionKeyType.getCurveName(); + ecKeyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH); + clientPublicKey = ecKeyPair.publicKeyInPEMFormat(); + } else { + // Initialize the RSA key pair only once and reuse it for future unwrap operations + if (decryptor == null) { + var encryptionKeypair = CryptoUtils.generateRSAKeypair(); + decryptor = new AsymDecryption(encryptionKeypair.getPrivate()); + clientPublicKey = CryptoUtils.getRSAPublicKeyPEM(encryptionKeypair.getPublic()); + } + } + RewrapRequestBody body = new RewrapRequestBody(); body.policy = policy; - body.clientPublicKey = publicKeyPEM; + body.clientPublicKey = clientPublicKey; body.keyAccess = keyAccess; - if (sessionKeyType != KeyType.RSA2048Key) { - var curveName =sessionKeyType.getCurveName(); - ecKeyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH); - body.clientPublicKey = ecKeyPair.publicKeyInPEMFormat(); - } - var requestBody = gson.toJson(body); - var claims = new JWTClaimsSet.Builder() .claim("requestBody", requestBody) .issueTime(Date.from(Instant.now())) 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 4f1a94f3..9c5cf010 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -38,4 +38,8 @@ public static KeyType fromString(String keyType) { } throw new IllegalArgumentException("No enum constant for key type: " + keyType); } + + public boolean isEc() { + return this != RSA2048Key; + } } \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java index 20928030..db19066e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -53,7 +53,8 @@ public interface KAS extends AutoCloseable { Config.KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve); - byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType); + byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, + KeyType sessionKeyType); byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL); 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 7dabe888..9be62212 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -330,8 +330,8 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA keyAccess.sid = splitID; keyAccess.schemaVersion = KEY_ACCESS_SECHMA_VERSION; - if (tdfConfig.wrappingKeyType != KeyType.RSA2048Key) { - var ecKeyWrappedKeyInfo =createECWrappedKey(tdfConfig, kasInfo, symKey); + if (tdfConfig.wrappingKeyType.isEc()) { + var ecKeyWrappedKeyInfo = createECWrappedKey(tdfConfig, kasInfo, symKey); keyAccess.wrappedKey = ecKeyWrappedKeyInfo.wrappedKey; keyAccess.ephemeralPublicKey = ecKeyWrappedKeyInfo.publicKey; keyAccess.keyType = kECWrapped; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java index 9cede0b2..b7699c7d 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/nanotdf/ECKeyPair.java @@ -41,21 +41,27 @@ public enum ECAlgorithm { private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); public enum NanoTDFECCurve { - SECP256R1("secp256r1"), - PRIME256V1("prime256v1"), - SECP384R1("secp384r1"), - SECP521R1("secp521r1"); + SECP256R1("secp256r1", KeyType.EC256Key), + PRIME256V1("prime256v1", KeyType.EC256Key), + SECP384R1("secp384r1", KeyType.EC384Key), + SECP521R1("secp521r1", KeyType.EC521Key); private String name; + private KeyType keyType; - NanoTDFECCurve(String curveName) { + NanoTDFECCurve(String curveName, KeyType keyType) { this.name = curveName; + this.keyType = keyType; } @Override public String toString() { return name; } + + public KeyType getKeyType() { + return keyType; + } } private KeyPair keyPair; @@ -146,17 +152,6 @@ public String privateKeyInPEMFormat() { return writer.toString(); } - public KeyType getKeyType() { - if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP256R1.toString())) { - return KeyType.EC256Key; - } else if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP384R1.toString())) { - return KeyType.EC384Key; - } else if (curveName.equalsIgnoreCase(NanoTDFECCurve.SECP521R1.toString())) { - return KeyType.EC521Key; - } - return null; - } - public int keySize() { return this.keyPair.getPrivate().getEncoded().length * 8; } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 4d72d637..0c1ed084 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -52,7 +52,7 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessio try { int index = Integer.parseInt(keyAccess.url); var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); - if (sessionKeyType != KeyType.RSA2048Key) { + if (sessionKeyType.isEc()) { var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey;