Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a8984e7
SDK NanoTDF - initial commit
mustyantsev May 13, 2024
01e419d
SymmetricAndPayloadConfig methods
mustyantsev May 14, 2024
5a64b82
more methods
mustyantsev May 14, 2024
b0c0779
methods for PolicyInfo
mustyantsev May 15, 2024
699e73b
fixes and test Header
mustyantsev May 16, 2024
bf69cf0
more tests
mustyantsev May 17, 2024
0c36cd4
NanoTDF crypto
sujankota May 17, 2024
dd6858a
Minor cleanup
sujankota May 17, 2024
bd10130
test nanotdf header
mustyantsev May 20, 2024
373389a
test nanotdf encrypt
mustyantsev May 22, 2024
b63b370
add more methods
mustyantsev May 24, 2024
fee0e20
read correct amount of bytes in ResourceLocator
mustyantsev May 24, 2024
1127fa5
read correct amount of bytes in binding
mustyantsev May 27, 2024
555621c
added test to read NanoTDF header
mustyantsev May 28, 2024
18e24c6
update test
mustyantsev May 28, 2024
6f04362
After merging from main
sujankota May 29, 2024
85c73b1
Encrypt done
sujankota Jun 3, 2024
2c1a9fe
Decrypt done
sujankota Jun 5, 2024
4e5d9ff
Merge branch 'main' into feature/nanotdf-encrypt
sujankota Jun 5, 2024
f723108
revert the pom.xml changes
sujankota Jun 5, 2024
758c60a
Fix the build
sujankota Jun 5, 2024
7e95114
Fix the unit test
sujankota Jun 5, 2024
409d64f
address comments and unit test
mustyantsev Jun 6, 2024
9abe7b1
feat(sdk): add CLI and integration tests (#64)
mkleene Jun 6, 2024
1d3aeee
fix(sdk): make sdk auto closeable (#63)
mkleene Jun 6, 2024
603edd6
fix(sdk): allow SDK to handle protocols in addresses (#70)
mkleene Jun 6, 2024
babb05e
fix the build
sujankota Jun 6, 2024
c7458be
merge main'
sujankota Jun 6, 2024
e5342ce
fix the build
sujankota Jun 6, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
Expand Down
69 changes: 62 additions & 7 deletions sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

public class AesGcm {
public static final int GCM_NONCE_LENGTH = 12; // in bytes
private static final int GCM_TAG_LENGTH = 16; // in bytes
public static final int GCM_TAG_LENGTH = 16; // in bytes
private static final String CIPHER_TRANSFORM = "AES/GCM/NoPadding";

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

/**
* <p>encrypt.</p>
*
* @param iv the IV vector
* @param authTagLen the length of the auth tag
* @param plaintext the plaintext byte array to encrypt
* @param offset where the input start
* @param len input length
* @return the encrypted text
*/
public byte[] encrypt(byte[] iv, int authTagLen, byte[] plaintext, int offset, int len) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);

GCMParameterSpec spec = new GCMParameterSpec(authTagLen * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);

byte[] cipherText = cipher.doFinal(plaintext, offset, len);
byte[] cipherTextWithNonce = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, cipherTextWithNonce, 0, iv.length);
System.arraycopy(cipherText, 0, cipherTextWithNonce, iv.length, cipherText.length);
return cipherTextWithNonce;
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error gcm decrypt", e);
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException("error gcm decrypt", e);
}
}

/**
* <p>decrypt.</p>
*
* @param cipherTextWithNonce the ciphertext with nonce to decrypt
* @return the decrypted text
*/
public byte[] decrypt(Encrypted cipherTextWithNonce) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, cipherTextWithNonce.iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(cipherTextWithNonce.ciphertext);
public byte[] decrypt(Encrypted cipherTextWithNonce) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, cipherTextWithNonce.iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(cipherTextWithNonce.ciphertext);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error gcm decrypt", e);
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException("error gcm decrypt", e);
}
}

/**
* <p>decrypt.</p>
*
* @param iv the IV vector
* @param authTagLen the length of the auth tag
* @param cipherData the cipherData byte array to decrypt
* @return the decrypted data
*/
public byte[] decrypt(byte[] iv, int authTagLen, byte[] cipherData) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
GCMParameterSpec spec = new GCMParameterSpec(authTagLen * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(cipherData);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error gcm decrypt", e);
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
throw new SDKException("error gcm decrypt", e);
}
}
}
67 changes: 67 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.opentdf.platform.sdk;

import io.opentdf.platform.sdk.nanotdf.ECCMode;
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
import io.opentdf.platform.sdk.nanotdf.SymmetricAndPayloadConfig;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -79,4 +83,67 @@ public static Consumer<TDFConfig> withMetaData(String metaData) {
public static Consumer<TDFConfig> withSegmentSize(int size) {
return (TDFConfig config) -> config.defaultSegmentSize = size;
}

public static class NanoTDFConfig {
public ECCMode eccMode;
public NanoTDFType.Cipher cipher;
public SymmetricAndPayloadConfig config;
public List<String> attributes;
public List<KASInfo> kasInfoList;

public NanoTDFConfig() {
this.eccMode = new ECCMode();
this.eccMode.setEllipticCurve(NanoTDFType.ECCurve.SECP256R1);
this.eccMode.setECDSABinding(false);

this.cipher = NanoTDFType.Cipher.AES_256_GCM_96_TAG;

this.config = new SymmetricAndPayloadConfig();
this.config.setHasSignature(false);
this.config.setSymmetricCipherType(NanoTDFType.Cipher.AES_256_GCM_96_TAG);

this.attributes = new ArrayList<>();
this.kasInfoList = new ArrayList<>();
}
}

public static NanoTDFConfig newNanoTDFConfig(Consumer<NanoTDFConfig>... options) {
NanoTDFConfig config = new NanoTDFConfig();
for (Consumer<NanoTDFConfig> option : options) {
option.accept(config);
}
return config;
}

public static Consumer<NanoTDFConfig> witDataAttributes(String... attributes) {
return (NanoTDFConfig config) -> {
Collections.addAll(config.attributes, attributes);
};
}

public static Consumer<NanoTDFConfig> withNanoKasInformation(KASInfo... kasInfoList) {
return (NanoTDFConfig config) -> {
Collections.addAll(config.kasInfoList, kasInfoList);
};
}

public static Consumer<NanoTDFConfig> withEllipticCurve(String curve) {
NanoTDFType.ECCurve ecCurve;
if (curve == null || curve.isEmpty()) {
ecCurve = NanoTDFType.ECCurve.SECP256R1; // default curve
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP384R1.toString()) == 0) {
ecCurve = NanoTDFType.ECCurve.SECP384R1;
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP521R1.toString()) == 0) {
ecCurve = NanoTDFType.ECCurve.SECP521R1;
} else if (curve.compareToIgnoreCase(NanoTDFType.ECCurve.SECP256R1.toString()) == 0) {
ecCurve = NanoTDFType.ECCurve.SECP256R1;
} else {
throw new IllegalArgumentException("The supplied curve string " + curve + " is not recognized.");
}
return (NanoTDFConfig config) -> config.eccMode.setEllipticCurve(ecCurve);
}

public static Consumer<NanoTDFConfig> WithECDSAPolicyBinding() {
return (NanoTDFConfig config) -> config.eccMode.setECDSABinding(false);
}
}
81 changes: 81 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.opentdf.platform.sdk;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
Expand All @@ -12,7 +13,11 @@
import io.opentdf.platform.kas.AccessServiceGrpc;
import io.opentdf.platform.kas.PublicKeyRequest;
import io.opentdf.platform.kas.RewrapRequest;
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
Expand Down Expand Up @@ -48,6 +53,13 @@ public KASClient(Function <String, ManagedChannel> channelFactory, RSAKey dpopKe
publicKeyPEM = CryptoUtils.getRSAPublicKeyPEM(encryptionKeypair.getPublic());
}

@Override
public String getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) {
return getStub(kasInfo.URL)
.publicKey(PublicKeyRequest.newBuilder().setAlgorithm(String.format("ec:%s", curve.toString())).build())
.getPublicKey();
}

@Override
public String getPublicKey(Config.KASInfo kasInfo) {
return getStub(kasInfo.URL)
Expand Down Expand Up @@ -97,6 +109,19 @@ static class RewrapRequestBody {
Manifest.KeyAccess keyAccess;
}

static class NanoTDFKeyAccess {
String header;
String type;
String url;
String protocol;
}

static class NanoTDFRewrapRequestBody {
String algorithm;
String clientPublicKey;
NanoTDFKeyAccess keyAccess;
}

private static final Gson gson = new Gson();

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

public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) {
ECKeyPair keyPair = new ECKeyPair(curve.toString(), ECKeyPair.ECAlgorithm.ECDH);

NanoTDFKeyAccess keyAccess = new NanoTDFKeyAccess();
keyAccess.header = header;
keyAccess.type = "remote";
keyAccess.url = kasURL;
keyAccess.protocol = "kas";

NanoTDFRewrapRequestBody body = new NanoTDFRewrapRequestBody();
body.algorithm = String.format("ec:%s", curve.toString());
body.clientPublicKey = keyPair.publicKeyInPEMFormat();
body.keyAccess = keyAccess;

var requestBody = gson.toJson(body);
var claims = new JWTClaimsSet.Builder()
.claim("requestBody", requestBody)
.issueTime(Date.from(Instant.now()))
.expirationTime(Date.from(Instant.now().plus(Duration.ofMinutes(1))))
.build();

var jws = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
SignedJWT jwt = new SignedJWT(jws, claims);
try {
jwt.sign(signer);
} catch (JOSEException e) {
throw new SDKException("error signing KAS request", e);
}

var request = RewrapRequest
.newBuilder()
.setSignedRequestToken(jwt.serialize())
.build();

var response = getStub(keyAccess.url).rewrap(request);
var wrappedKey = response.getEntityWrappedKey().toByteArray();

// Generate symmetric key
byte[] symmetricKey = ECKeyPair.computeECDHKey(ECKeyPair.publicKeyFromPem(response.getSessionPublicKey()),
keyPair.getPrivateKey());

// Generate HKDF key
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new SDKException("error creating SHA-256 message digest", e);
}
byte[] hashOfSalt = digest.digest(NanoTDF.MAGIC_NUMBER_AND_VERSION);
byte[] key = ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey);

AesGcm gcm = new AesGcm(key);
AesGcm.Encrypted encrypted = new AesGcm.Encrypted(wrappedKey);
return gcm.decrypt(encrypted);
}

private final HashMap<String, CacheEntry> stubs = new HashMap<>();
private static class CacheEntry {
final ManagedChannel channel;
Expand Down
Loading