Skip to content

Commit 5be59c4

Browse files
authored
Merge branch 'main' into feature/code-coverage
2 parents ea5d431 + 6485326 commit 5be59c4

26 files changed

+2423
-9
lines changed

sdk/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@
8282
<version>4.13.1</version>
8383
<scope>test</scope>
8484
</dependency>
85+
<dependency>
86+
<groupId>org.bouncycastle</groupId>
87+
<artifactId>bcpkix-jdk18on</artifactId>
88+
<version>1.78.1</version>
89+
</dependency>
8590
<dependency>
8691
<groupId>com.google.code.gson</groupId>
8792
<artifactId>gson</artifactId>

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

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
public class AesGcm {
1616
public static final int GCM_NONCE_LENGTH = 12; // in bytes
17-
private static final int GCM_TAG_LENGTH = 16; // in bytes
17+
public static final int GCM_TAG_LENGTH = 16; // in bytes
1818
private static final String CIPHER_TRANSFORM = "AES/GCM/NoPadding";
1919

2020
private final SecretKey key;
@@ -115,17 +115,72 @@ public Encrypted encrypt(byte[] plaintext, int offset, int len) {
115115
return new Encrypted(nonce, cipherText);
116116
}
117117

118+
/**
119+
* <p>encrypt.</p>
120+
*
121+
* @param iv the IV vector
122+
* @param authTagLen the length of the auth tag
123+
* @param plaintext the plaintext byte array to encrypt
124+
* @param offset where the input start
125+
* @param len input length
126+
* @return the encrypted text
127+
*/
128+
public byte[] encrypt(byte[] iv, int authTagLen, byte[] plaintext, int offset, int len) {
129+
try {
130+
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
131+
132+
GCMParameterSpec spec = new GCMParameterSpec(authTagLen * 8, iv);
133+
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
134+
135+
byte[] cipherText = cipher.doFinal(plaintext, offset, len);
136+
byte[] cipherTextWithNonce = new byte[iv.length + cipherText.length];
137+
System.arraycopy(iv, 0, cipherTextWithNonce, 0, iv.length);
138+
System.arraycopy(cipherText, 0, cipherTextWithNonce, iv.length, cipherText.length);
139+
return cipherTextWithNonce;
140+
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
141+
throw new RuntimeException("error gcm decrypt", e);
142+
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
143+
throw new RuntimeException("error gcm decrypt", e);
144+
}
145+
}
146+
118147
/**
119148
* <p>decrypt.</p>
120149
*
121150
* @param cipherTextWithNonce the ciphertext with nonce to decrypt
122151
* @return the decrypted text
123152
*/
124-
public byte[] decrypt(Encrypted cipherTextWithNonce) throws NoSuchPaddingException, NoSuchAlgorithmException,
125-
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
126-
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
127-
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, cipherTextWithNonce.iv);
128-
cipher.init(Cipher.DECRYPT_MODE, key, spec);
129-
return cipher.doFinal(cipherTextWithNonce.ciphertext);
153+
public byte[] decrypt(Encrypted cipherTextWithNonce) {
154+
try {
155+
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
156+
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, cipherTextWithNonce.iv);
157+
cipher.init(Cipher.DECRYPT_MODE, key, spec);
158+
return cipher.doFinal(cipherTextWithNonce.ciphertext);
159+
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
160+
throw new RuntimeException("error gcm decrypt", e);
161+
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
162+
throw new RuntimeException("error gcm decrypt", e);
163+
}
164+
}
165+
166+
/**
167+
* <p>decrypt.</p>
168+
*
169+
* @param iv the IV vector
170+
* @param authTagLen the length of the auth tag
171+
* @param cipherData the cipherData byte array to decrypt
172+
* @return the decrypted data
173+
*/
174+
public byte[] decrypt(byte[] iv, int authTagLen, byte[] cipherData) {
175+
try {
176+
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
177+
GCMParameterSpec spec = new GCMParameterSpec(authTagLen * 8, iv);
178+
cipher.init(Cipher.DECRYPT_MODE, key, spec);
179+
return cipher.doFinal(cipherData);
180+
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
181+
throw new RuntimeException("error gcm decrypt", e);
182+
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
183+
throw new SDKException("error gcm decrypt", e);
184+
}
130185
}
131186
}

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.opentdf.platform.sdk;
22

3+
import io.opentdf.platform.sdk.nanotdf.ECCMode;
4+
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
5+
import io.opentdf.platform.sdk.nanotdf.SymmetricAndPayloadConfig;
6+
37
import java.util.ArrayList;
48
import java.util.Collections;
59
import java.util.List;
@@ -79,4 +83,67 @@ public static Consumer<TDFConfig> withMetaData(String metaData) {
7983
public static Consumer<TDFConfig> withSegmentSize(int size) {
8084
return (TDFConfig config) -> config.defaultSegmentSize = size;
8185
}
86+
87+
public static class NanoTDFConfig {
88+
public ECCMode eccMode;
89+
public NanoTDFType.Cipher cipher;
90+
public SymmetricAndPayloadConfig config;
91+
public List<String> attributes;
92+
public List<KASInfo> kasInfoList;
93+
94+
public NanoTDFConfig() {
95+
this.eccMode = new ECCMode();
96+
this.eccMode.setEllipticCurve(NanoTDFType.ECCurve.SECP256R1);
97+
this.eccMode.setECDSABinding(false);
98+
99+
this.cipher = NanoTDFType.Cipher.AES_256_GCM_96_TAG;
100+
101+
this.config = new SymmetricAndPayloadConfig();
102+
this.config.setHasSignature(false);
103+
this.config.setSymmetricCipherType(NanoTDFType.Cipher.AES_256_GCM_96_TAG);
104+
105+
this.attributes = new ArrayList<>();
106+
this.kasInfoList = new ArrayList<>();
107+
}
108+
}
109+
110+
public static NanoTDFConfig newNanoTDFConfig(Consumer<NanoTDFConfig>... options) {
111+
NanoTDFConfig config = new NanoTDFConfig();
112+
for (Consumer<NanoTDFConfig> option : options) {
113+
option.accept(config);
114+
}
115+
return config;
116+
}
117+
118+
public static Consumer<NanoTDFConfig> witDataAttributes(String... attributes) {
119+
return (NanoTDFConfig config) -> {
120+
Collections.addAll(config.attributes, attributes);
121+
};
122+
}
123+
124+
public static Consumer<NanoTDFConfig> withNanoKasInformation(KASInfo... kasInfoList) {
125+
return (NanoTDFConfig config) -> {
126+
Collections.addAll(config.kasInfoList, kasInfoList);
127+
};
128+
}
129+
130+
public static Consumer<NanoTDFConfig> withEllipticCurve(String curve) {
131+
NanoTDFType.ECCurve ecCurve;
132+
if (curve == null || curve.isEmpty()) {
133+
ecCurve = NanoTDFType.ECCurve.SECP256R1; // default curve
134+
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP384R1.toString()) == 0) {
135+
ecCurve = NanoTDFType.ECCurve.SECP384R1;
136+
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP521R1.toString()) == 0) {
137+
ecCurve = NanoTDFType.ECCurve.SECP521R1;
138+
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP256R1.toString()) == 0) {
139+
ecCurve = NanoTDFType.ECCurve.SECP256R1;
140+
} else {
141+
throw new IllegalArgumentException("The supplied curve string " + curve + " is not recognized.");
142+
}
143+
return (NanoTDFConfig config) -> config.eccMode.setEllipticCurve(ecCurve);
144+
}
145+
146+
public static Consumer<NanoTDFConfig> WithECDSAPolicyBinding() {
147+
return (NanoTDFConfig config) -> config.eccMode.setECDSABinding(false);
148+
}
82149
}

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.opentdf.platform.sdk;
22

33
import com.google.gson.Gson;
4+
import com.google.gson.annotations.SerializedName;
45
import com.nimbusds.jose.JOSEException;
56
import com.nimbusds.jose.JWSAlgorithm;
67
import com.nimbusds.jose.JWSHeader;
@@ -12,7 +13,11 @@
1213
import io.opentdf.platform.kas.AccessServiceGrpc;
1314
import io.opentdf.platform.kas.PublicKeyRequest;
1415
import io.opentdf.platform.kas.RewrapRequest;
16+
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
17+
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
1518

19+
import java.security.MessageDigest;
20+
import java.security.NoSuchAlgorithmException;
1621
import java.net.MalformedURLException;
1722
import java.net.URL;
1823
import java.time.Duration;
@@ -48,6 +53,13 @@ public KASClient(Function <String, ManagedChannel> channelFactory, RSAKey dpopKe
4853
publicKeyPEM = CryptoUtils.getRSAPublicKeyPEM(encryptionKeypair.getPublic());
4954
}
5055

56+
@Override
57+
public String getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) {
58+
return getStub(kasInfo.URL)
59+
.publicKey(PublicKeyRequest.newBuilder().setAlgorithm(String.format("ec:%s", curve.toString())).build())
60+
.getPublicKey();
61+
}
62+
5163
@Override
5264
public String getPublicKey(Config.KASInfo kasInfo) {
5365
return getStub(kasInfo.URL)
@@ -97,6 +109,19 @@ static class RewrapRequestBody {
97109
Manifest.KeyAccess keyAccess;
98110
}
99111

112+
static class NanoTDFKeyAccess {
113+
String header;
114+
String type;
115+
String url;
116+
String protocol;
117+
}
118+
119+
static class NanoTDFRewrapRequestBody {
120+
String algorithm;
121+
String clientPublicKey;
122+
NanoTDFKeyAccess keyAccess;
123+
}
124+
100125
private static final Gson gson = new Gson();
101126

102127
@Override
@@ -130,6 +155,62 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) {
130155
return decryptor.decrypt(wrappedKey);
131156
}
132157

158+
public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) {
159+
ECKeyPair keyPair = new ECKeyPair(curve.toString(), ECKeyPair.ECAlgorithm.ECDH);
160+
161+
NanoTDFKeyAccess keyAccess = new NanoTDFKeyAccess();
162+
keyAccess.header = header;
163+
keyAccess.type = "remote";
164+
keyAccess.url = kasURL;
165+
keyAccess.protocol = "kas";
166+
167+
NanoTDFRewrapRequestBody body = new NanoTDFRewrapRequestBody();
168+
body.algorithm = String.format("ec:%s", curve.toString());
169+
body.clientPublicKey = keyPair.publicKeyInPEMFormat();
170+
body.keyAccess = keyAccess;
171+
172+
var requestBody = gson.toJson(body);
173+
var claims = new JWTClaimsSet.Builder()
174+
.claim("requestBody", requestBody)
175+
.issueTime(Date.from(Instant.now()))
176+
.expirationTime(Date.from(Instant.now().plus(Duration.ofMinutes(1))))
177+
.build();
178+
179+
var jws = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
180+
SignedJWT jwt = new SignedJWT(jws, claims);
181+
try {
182+
jwt.sign(signer);
183+
} catch (JOSEException e) {
184+
throw new SDKException("error signing KAS request", e);
185+
}
186+
187+
var request = RewrapRequest
188+
.newBuilder()
189+
.setSignedRequestToken(jwt.serialize())
190+
.build();
191+
192+
var response = getStub(keyAccess.url).rewrap(request);
193+
var wrappedKey = response.getEntityWrappedKey().toByteArray();
194+
195+
// Generate symmetric key
196+
byte[] symmetricKey = ECKeyPair.computeECDHKey(ECKeyPair.publicKeyFromPem(response.getSessionPublicKey()),
197+
keyPair.getPrivateKey());
198+
199+
// Generate HKDF key
200+
MessageDigest digest;
201+
try {
202+
digest = MessageDigest.getInstance("SHA-256");
203+
} catch (NoSuchAlgorithmException e) {
204+
throw new SDKException("error creating SHA-256 message digest", e);
205+
}
206+
byte[] hashOfSalt = digest.digest(NanoTDF.MAGIC_NUMBER_AND_VERSION);
207+
byte[] key = ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey);
208+
209+
AesGcm gcm = new AesGcm(key);
210+
AesGcm.Encrypted encrypted = new AesGcm.Encrypted(wrappedKey);
211+
return gcm.decrypt(encrypted);
212+
}
213+
133214
private final HashMap<String, CacheEntry> stubs = new HashMap<>();
134215
private static class CacheEntry {
135216
final ManagedChannel channel;

0 commit comments

Comments
 (0)