Skip to content

Commit 3bdffa8

Browse files
committed
feat(sdk): EC-wrapped key support for ZTDF
1 parent a8d2b5c commit 3bdffa8

File tree

16 files changed

+388
-109
lines changed

16 files changed

+388
-109
lines changed

examples/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
<groupId>org.apache.logging.log4j</groupId>
6262
<artifactId>log4j-core</artifactId>
6363
</dependency>
64+
<dependency>
65+
<groupId>commons-cli</groupId>
66+
<artifactId>commons-cli</artifactId>
67+
<version>1.4</version>
68+
</dependency>
6469
<dependency>
6570
<groupId>org.apache.logging.log4j</groupId>
6671
<artifactId>log4j-api</artifactId>
Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
11
package io.opentdf.platform;
2+
23
import io.opentdf.platform.sdk.*;
34
import java.nio.file.StandardOpenOption;
45
import java.nio.channels.FileChannel;
56
import java.nio.file.Path;
67
import java.nio.file.Paths;
7-
88
import com.nimbusds.jose.JOSEException;
99
import java.io.IOException;
10-
import java.util.concurrent.ExecutionException;
1110
import java.security.InvalidAlgorithmParameterException;
1211
import java.security.InvalidKeyException;
1312
import java.security.NoSuchAlgorithmException;
1413
import java.text.ParseException;
1514
import javax.crypto.BadPaddingException;
1615
import javax.crypto.IllegalBlockSizeException;
1716
import javax.crypto.NoSuchPaddingException;
17+
import org.apache.commons.cli.*;
1818
import org.apache.commons.codec.DecoderException;
1919

20-
2120
public class DecryptExample {
2221
public static void main(String[] args) throws IOException,
23-
InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException,
24-
BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC,
25-
JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
22+
InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException,
23+
BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC,
24+
JOSEException, ParseException, NoSuchAlgorithmException, DecoderException, org.apache.commons.cli.ParseException {
25+
26+
// Create Options object
27+
Options options = new Options();
28+
29+
// Add rewrap encapsulation algorithm option
30+
options.addOption(Option.builder("A")
31+
.longOpt("rewrap-encapsulation-algorithm")
32+
.hasArg()
33+
.desc("Key wrap response algorithm algorithm:parameters")
34+
.build());
35+
36+
// Parse command line arguments
37+
CommandLineParser parser = new DefaultParser();
38+
CommandLine cmd = parser.parse(options, args);
39+
40+
// Get the rewrap encapsulation algorithm
41+
String rewrapEncapsulationAlgorithm = cmd.getOptionValue("rewrap-encapsulation-algorithm", "rsa:2048");
42+
var sessionKeyType = KeyType.fromString(rewrapEncapsulationAlgorithm.toLowerCase());
43+
2644

2745
String clientId = "opentdf";
2846
String clientSecret = "secret";
@@ -35,8 +53,11 @@ public static void main(String[] args) throws IOException,
3553

3654
Path path = Paths.get("my.ciphertext");
3755
try (var in = FileChannel.open(path, StandardOpenOption.READ)) {
38-
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
56+
var reader = new TDF().loadTDF(in, sdk.getServices().kas(), Config.newTDFReaderConfig(Config.WithSessionKeyType(sessionKeyType)));
3957
reader.readPayload(System.out);
4058
}
59+
60+
// Print the rewrap encapsulation algorithm
61+
System.out.println("Rewrap Encapsulation Algorithm: " + rewrapEncapsulationAlgorithm);
4162
}
42-
}
63+
}
Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
package io.opentdf.platform;
2+
23
import io.opentdf.platform.sdk.*;
34
import java.io.ByteArrayInputStream;
4-
import java.io.BufferedOutputStream;
55
import java.nio.charset.StandardCharsets;
66
import java.io.FileOutputStream;
7-
87
import com.nimbusds.jose.JOSEException;
8+
import org.apache.commons.cli.*;
99
import org.apache.commons.codec.DecoderException;
10-
1110
import java.io.IOException;
1211
import java.util.concurrent.ExecutionException;
1312

1413
public class EncryptExample {
15-
public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException, DecoderException {
14+
public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException,
15+
InterruptedException, ExecutionException, DecoderException, ParseException {
16+
// Create Options object
17+
Options options = new Options();
18+
19+
// Add key encapsulation algorithm option
20+
options.addOption(Option.builder("A")
21+
.longOpt("key-encapsulation-algorithm")
22+
.hasArg()
23+
.desc("Key wrap algorithm algorithm:parameters")
24+
.build());
25+
26+
// Parse command line arguments
27+
CommandLineParser parser = new DefaultParser();
28+
CommandLine cmd = parser.parse(options, args);
29+
30+
// Get the key encapsulation algorithm
31+
String keyEncapsulationAlgorithm = cmd.getOptionValue("key-encapsulation-algorithm", "rsa:2048");
32+
1633
String clientId = "opentdf";
1734
String clientSecret = "secret";
1835
String platformEndpoint = "localhost:8080";
@@ -25,17 +42,19 @@ public static void main(String[] args) throws IOException, JOSEException, AutoCo
2542
var kasInfo = new Config.KASInfo();
2643
kasInfo.URL = "http://localhost:8080/kas";
2744

28-
var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo), Config.withDataAttributes("https://example.com/attr/color/value/red"));
29-
45+
var wrappingKeyType = KeyType.fromString(keyEncapsulationAlgorithm.toLowerCase());
46+
var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo),
47+
Config.withDataAttributes("https://example.com/attr/color/value/red"),
48+
Config.WithWrappingKeyAlg(wrappingKeyType));
3049
String str = "Hello, World!";
31-
50+
3251
// Convert String to InputStream
3352
var in = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
3453

3554
FileOutputStream fos = new FileOutputStream("my.ciphertext");
3655

3756
new TDF().createTDF(in, fos, tdfConfig,
38-
sdk.getServices().kas(),
39-
sdk.getServices().attributes());
57+
sdk.getServices().kas(),
58+
sdk.getServices().attributes());
4059
}
41-
}
60+
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
import io.opentdf.platform.sdk.nanotdf.SymmetricAndPayloadConfig;
88

99
import io.opentdf.platform.policy.Value;
10-
import org.bouncycastle.oer.its.ieee1609dot2.HeaderInfo;
1110

1211
import java.util.*;
13-
import java.util.concurrent.atomic.AtomicInteger;
1412
import java.util.function.Consumer;
1513

1614
/**
@@ -99,12 +97,14 @@ public static class TDFReaderConfig {
9997
// Optional Map of Assertion Verification Keys
10098
AssertionVerificationKeys assertionVerificationKeys = new AssertionVerificationKeys();
10199
boolean disableAssertionVerification;
100+
KeyType sessionKeyType;;
102101
}
103102

104103
@SafeVarargs
105104
public static TDFReaderConfig newTDFReaderConfig(Consumer<TDFReaderConfig>... options) {
106105
TDFReaderConfig config = new TDFReaderConfig();
107106
config.disableAssertionVerification = false;
107+
config.sessionKeyType = KeyType.RSA2048Key;
108108
for (Consumer<TDFReaderConfig> option : options) {
109109
option.accept(config);
110110
}
@@ -120,6 +120,9 @@ public static Consumer<TDFReaderConfig> withDisableAssertionVerification(boolean
120120
return (TDFReaderConfig config) -> config.disableAssertionVerification = disable;
121121
}
122122

123+
public static Consumer<TDFReaderConfig> WithSessionKeyType(KeyType keyType) {
124+
return (TDFReaderConfig config) -> config.sessionKeyType = keyType;
125+
}
123126
public static class TDFConfig {
124127
public Boolean autoconfigure;
125128
public int defaultSegmentSize;
@@ -136,6 +139,7 @@ public static class TDFConfig {
136139
public List<io.opentdf.platform.sdk.AssertionConfig> assertionConfigList;
137140
public String mimeType;
138141
public List<Autoconfigure.KeySplitStep> splitPlan;
142+
public KeyType wrappingKeyType;
139143

140144
public TDFConfig() {
141145
this.autoconfigure = true;
@@ -149,6 +153,7 @@ public TDFConfig() {
149153
this.assertionConfigList = new ArrayList<>();
150154
this.mimeType = DEFAULT_MIME_TYPE;
151155
this.splitPlan = new ArrayList<>();
156+
this.wrappingKeyType = KeyType.RSA2048Key;
152157
}
153158
}
154159

@@ -246,6 +251,10 @@ public static Consumer<TDFConfig> withAutoconfigure(boolean enable) {
246251
};
247252
}
248253

254+
public static Consumer<TDFConfig> WithWrappingKeyAlg(KeyType keyType) {
255+
return (TDFConfig config) -> config.wrappingKeyType = keyType;
256+
}
257+
249258
// public static Consumer<TDFConfig> withDisableEncryption() {
250259
// return (TDFConfig config) -> config.enableEncryption = false;
251260
// }

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import javax.crypto.Mac;
44
import javax.crypto.spec.SecretKeySpec;
55
import java.security.*;
6+
import java.security.spec.ECGenParameterSpec;
67
import java.util.Base64;
78

89
/**
@@ -39,6 +40,30 @@ public static KeyPair generateRSAKeypair() {
3940
return kpg.generateKeyPair();
4041
}
4142

43+
public static KeyPair generateECKeypair(String curveName) {
44+
KeyPairGenerator kpg;
45+
try {
46+
kpg = KeyPairGenerator.getInstance("EC");
47+
ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName);
48+
kpg.initialize(ecSpec);
49+
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
50+
throw new SDKException("error creating EC keypair", e);
51+
}
52+
return kpg.generateKeyPair();
53+
}
54+
55+
public static String getPublicKeyPEM(PublicKey publicKey) {
56+
return "-----BEGIN PUBLIC KEY-----\r\n" +
57+
Base64.getMimeEncoder().encodeToString(publicKey.getEncoded()) +
58+
"\r\n-----END PUBLIC KEY-----";
59+
}
60+
61+
public static String getPrivateKeyPEM(PrivateKey privateKey) {
62+
return "-----BEGIN PRIVATE KEY-----\r\n" +
63+
Base64.getMimeEncoder().encodeToString(privateKey.getEncoded()) +
64+
"\r\n-----END PRIVATE KEY-----";
65+
}
66+
4267
public static String getRSAPublicKeyPEM(PublicKey publicKey) {
4368
if (!"RSA".equals(publicKey.getAlgorithm())) {
4469
throw new IllegalArgumentException("can't get public key PEM for algorithm [" + publicKey.getAlgorithm() + "]");

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import io.opentdf.platform.kas.RewrapRequest;
1818
import io.opentdf.platform.kas.RewrapResponse;
1919
import io.opentdf.platform.sdk.Config.KASInfo;
20+
import io.opentdf.platform.sdk.nanotdf.CryptoKeyPair;
2021
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
2122
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
2223
import io.opentdf.platform.sdk.TDF.KasBadRequestException;
2324

25+
import java.nio.charset.StandardCharsets;
2426
import java.security.MessageDigest;
2527
import java.security.NoSuchAlgorithmException;
2628
import java.net.MalformedURLException;
@@ -32,6 +34,7 @@
3234
import java.util.HashMap;
3335
import java.util.function.Function;
3436

37+
import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT;
3538
import static java.lang.String.format;
3639

3740
/**
@@ -45,7 +48,7 @@ public class KASClient implements SDK.KAS {
4548
private final RSASSASigner signer;
4649
private final AsymDecryption decryptor;
4750
private final String publicKeyPEM;
48-
51+
private CryptoKeyPair keyPair;
4952
private KASKeyCache kasKeyCache;
5053

5154
/***
@@ -86,8 +89,9 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) {
8689
if (cachedValue != null) {
8790
return cachedValue;
8891
}
89-
PublicKeyResponse resp = getStub(kasInfo.URL).publicKey(PublicKeyRequest.getDefaultInstance());
90-
92+
var resp = getStub(kasInfo.URL)
93+
.publicKey(
94+
PublicKeyRequest.newBuilder().setAlgorithm(kasInfo.Algorithm).build());
9195
var kiCopy = new Config.KASInfo();
9296
kiCopy.KID = resp.getKid();
9397
kiCopy.PublicKey = resp.getPublicKey();
@@ -161,11 +165,19 @@ static class NanoTDFRewrapRequestBody {
161165
private static final Gson gson = new Gson();
162166

163167
@Override
164-
public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) {
168+
public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) {
169+
ECKeyPair ecKeyPair = null;
165170
RewrapRequestBody body = new RewrapRequestBody();
166171
body.policy = policy;
167172
body.clientPublicKey = publicKeyPEM;
168173
body.keyAccess = keyAccess;
174+
175+
if (sessionKeyType != KeyType.RSA2048Key) {
176+
var curveName =sessionKeyType.getCurveName();
177+
ecKeyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH);
178+
body.clientPublicKey = ecKeyPair.publicKeyInPEMFormat();
179+
}
180+
169181
var requestBody = gson.toJson(body);
170182

171183
var claims = new JWTClaimsSet.Builder()
@@ -190,7 +202,24 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) {
190202
try {
191203
response = getStub(keyAccess.url).rewrap(request);
192204
var wrappedKey = response.getEntityWrappedKey().toByteArray();
193-
return decryptor.decrypt(wrappedKey);
205+
if (sessionKeyType != KeyType.RSA2048Key) {
206+
207+
if (ecKeyPair == null) {
208+
throw new SDKException("ECKeyPair is null. Unable to proceed with the unwrap operation.");
209+
}
210+
211+
var kasEphemeralPublicKey = response.getSessionPublicKey();
212+
var publicKey = ECKeyPair.publicKeyFromPem(kasEphemeralPublicKey);
213+
byte[] symKey = ECKeyPair.computeECDHKey(publicKey, ecKeyPair.getPrivateKey());
214+
215+
var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey);
216+
217+
AesGcm gcm = new AesGcm(sessionKey);
218+
AesGcm.Encrypted encrypted = new AesGcm.Encrypted(wrappedKey);
219+
return gcm.decrypt(encrypted);
220+
} else {
221+
return decryptor.decrypt(wrappedKey);
222+
}
194223
} catch (StatusRuntimeException e) {
195224
if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
196225
// 400 Bad Request
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.opentdf.platform.sdk;
2+
3+
public enum KeyType {
4+
RSA2048Key("rsa:2048"),
5+
EC256Key("ec:secp256r1"),
6+
EC384Key("ec:secp384r1"),
7+
EC521Key("ec:secp521r1");
8+
9+
private final String keyType;
10+
11+
KeyType(String keyType) {
12+
this.keyType = keyType;
13+
}
14+
15+
@Override
16+
public String toString() {
17+
return keyType;
18+
}
19+
20+
public String getCurveName() {
21+
switch (this) {
22+
case EC256Key:
23+
return "secp256r1";
24+
case EC384Key:
25+
return "secp384r1";
26+
case EC521Key:
27+
return "secp521r1";
28+
default:
29+
throw new IllegalArgumentException("Unsupported key type: " + this);
30+
}
31+
}
32+
33+
public static KeyType fromString(String keyType) {
34+
for (KeyType type : KeyType.values()) {
35+
if (type.keyType.equalsIgnoreCase(keyType)) {
36+
return type;
37+
}
38+
}
39+
throw new IllegalArgumentException("No enum constant for key type: " + keyType);
40+
}
41+
}

0 commit comments

Comments
 (0)