Skip to content

feat(sdk): get e2e rewrap working #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
76ee941
SDK Encrypt (with mocked rewrap)
mustyantsev May 8, 2024
5d003a0
Manifest and some cleanup
sujankota May 8, 2024
a2eebd9
Remove file
sujankota May 8, 2024
1196a9a
Implement tdf encrypt
sujankota May 10, 2024
bbc90d0
update total bytes written
mustyantsev May 10, 2024
6404832
update tdfObject size
mustyantsev May 10, 2024
f2a9b2c
Implement the decrypt
sujankota May 12, 2024
ba170db
add code to provide access tokens dynamically to KAS
mkleene May 20, 2024
a3dbf10
Merge branch 'main' into feature/add-kas-authentication
mkleene May 20, 2024
3c2816a
rename
mkleene May 20, 2024
1e1e09c
Merge remote-tracking branch 'origin/feature/add-kas-authentication' …
mkleene May 20, 2024
7987913
formatting and mildly improve test
mkleene May 22, 2024
37332bd
Merge remote-tracking branch 'origin/feature/sdk-encrypt' into featur…
mkleene May 22, 2024
b9e4973
get test working
mkleene May 23, 2024
4a733c9
create a rewrap request
mkleene May 23, 2024
a56f1b3
get e2e working
mkleene May 23, 2024
1a3caa1
turn down logging
mkleene May 23, 2024
dab02fc
Update KASClient.java
mkleene May 23, 2024
164969c
fix this
mkleene May 23, 2024
229d477
Merge remote-tracking branch 'origin/feature/tdf-e2e' into feature/td…
mkleene May 23, 2024
9edea3e
Merge remote-tracking branch 'origin/main' into feature/tdf-e2e
mkleene May 24, 2024
832ae41
add test for KAS client
mkleene May 28, 2024
52aeb30
Update ZipWriterTest.java
mkleene May 28, 2024
c67330d
Merge remote-tracking branch 'origin/main' into feature/tdf-e2e
mkleene May 28, 2024
f504881
Merge remote-tracking branch 'origin/feature/tdf-e2e' into feature/td…
mkleene May 28, 2024
6488c6a
add assertion
mkleene May 28, 2024
7ec3989
minor fixes
mkleene May 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package io.opentdf.platform.sdk;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class AsymDecryption {
private PrivateKey privateKey;
private final PrivateKey privateKey;
private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";
private static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";
private static final String CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
Expand All @@ -16,7 +20,7 @@ public class AsymDecryption {
*
* @param privateKeyInPem a Private Key in PEM format
*/
public AsymDecryption(String privateKeyInPem) throws Exception {
public AsymDecryption(String privateKeyInPem) {
String privateKeyPEM = privateKeyInPem
.replace(PRIVATE_KEY_HEADER, "")
.replace(PRIVATE_KEY_FOOTER, "")
Expand All @@ -25,8 +29,21 @@ public AsymDecryption(String privateKeyInPem) throws Exception {
byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);

PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
this.privateKey = kf.generatePrivate(spec);
KeyFactory kf = null;
try {
kf = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try {
this.privateKey = kf.generatePrivate(spec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}

public AsymDecryption(PrivateKey privateKey) {
this.privateKey = privateKey;
}

/**
Expand All @@ -35,13 +52,26 @@ public AsymDecryption(String privateKeyInPem) throws Exception {
* @param data the data to decrypt
* @return the decrypted data
*/
public byte[] decrypt(byte[] data) throws Exception {
public byte[] decrypt(byte[] data) {
if (this.privateKey == null) {
throw new Exception("Failed to decrypt, private key is empty");
throw new SDKException("Failed to decrypt, private key is empty");
}

Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
cipher.init(Cipher.DECRYPT_MODE, this.privateKey);
return cipher.doFinal(data);
Cipher cipher;
try {
cipher = Cipher.getInstance(CIPHER_TRANSFORM);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new SDKException("error getting instance of cipher", e);
}
try {
cipher.init(Cipher.DECRYPT_MODE, this.privateKey);
} catch (InvalidKeyException e) {
throw new SDKException("error initializing cipher", e);
}
try {
return cipher.doFinal(data);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new SDKException("error performing decryption", e);
}
}
}
54 changes: 39 additions & 15 deletions sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package io.opentdf.platform.sdk;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Objects;

public class AsymEncryption {
private PublicKey publicKey;
private final PublicKey publicKey;
private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----";
private static final String PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----";
private static final String CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
Expand All @@ -16,16 +21,30 @@ public class AsymEncryption {
*
* @param publicKeyInPem a Public Key in PEM format
*/
public AsymEncryption(String publicKeyInPem) throws Exception {
public AsymEncryption(String publicKeyInPem) {
publicKeyInPem = publicKeyInPem
.replace(PUBLIC_KEY_HEADER, "")
.replace(PUBLIC_KEY_FOOTER, "")
.replaceAll("\\s", "");

byte[] decoded = Base64.getDecoder().decode(publicKeyInPem);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
this.publicKey = kf.generatePublic(spec);
KeyFactory kf;
try {
kf = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new SDKException("RSA is not a valid algorithm!!!???!!!", e);
}

try {
this.publicKey = kf.generatePublic(spec);
} catch (InvalidKeySpecException e) {
throw new SDKException("error creating asymmetric encryption", e);
}
}

public AsymEncryption(PublicKey publicKey) {
this.publicKey = Objects.requireNonNull(publicKey);
}

/**
Expand All @@ -34,25 +53,30 @@ public AsymEncryption(String publicKeyInPem) throws Exception {
* @param data the data to encrypt
* @return the encrypted data
*/
public byte[] encrypt(byte[] data) throws Exception {
if (this.publicKey == null) {
throw new Exception("Failed to encrypt, public key is empty");
public byte[] encrypt(byte[] data) {
Cipher cipher;
try {
cipher = Cipher.getInstance(CIPHER_TRANSFORM);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new SDKException("error getting instance of cipher during encryption", e);
}
try {
cipher.init(Cipher.ENCRYPT_MODE, this.publicKey);
} catch (InvalidKeyException e) {
throw new SDKException("error encrypting with private key", e);
}
try {
return cipher.doFinal(data);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new SDKException("error performing encryption", e);
}

Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, this.publicKey);
return cipher.doFinal(data);
}

/**
* <p>publicKeyInPemFormat.</p>
* @return the public key in PEM format
*/
public String publicKeyInPemFormat() throws Exception {
if (this.publicKey == null) {
throw new Exception("Failed to generate PEM formatted public key");
}

String publicKeyPem = Base64.getEncoder().encodeToString(this.publicKey.getEncoded());
return PUBLIC_KEY_HEADER + '\n' + publicKeyPem + '\n' + PUBLIC_KEY_FOOTER + '\n';
}
Expand Down
28 changes: 27 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.Base64;

public class CryptoUtils {
private static final int KEYPAIR_SIZE = 2048;

public static byte[] CalculateSHA256Hmac(byte[] key, byte[] data) throws NoSuchAlgorithmException,
InvalidKeyException {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
Expand All @@ -15,4 +20,25 @@ public static byte[] CalculateSHA256Hmac(byte[] key, byte[] data) throws NoSuchA

return sha256_HMAC.doFinal(data);
}

public static KeyPair generateRSAKeypair() {
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new SDKException("error creating keypair", e);
}
kpg.initialize(KEYPAIR_SIZE);
return kpg.generateKeyPair();
}

public static String getRSAPublicKeyPEM(PublicKey publicKey) {
if (!"RSA".equals(publicKey.getAlgorithm())) {
throw new IllegalArgumentException("can't get public key PEM for algorithm [" + publicKey.getAlgorithm() + "]");
}

return "-----BEGIN PUBLIC KEY-----\r\n" +
Base64.getMimeEncoder().encodeToString(publicKey.getEncoded()) +
"\r\n-----END PUBLIC KEY-----";
}
}
113 changes: 98 additions & 15 deletions sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,124 @@
package io.opentdf.platform.sdk;

import io.grpc.Channel;
import com.google.gson.Gson;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import io.grpc.ManagedChannel;
import io.opentdf.platform.kas.AccessServiceGrpc;
import io.opentdf.platform.kas.PublicKeyRequest;
import io.opentdf.platform.kas.RewrapRequest;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

public class KASClient implements SDK.KAS {
public class KASClient implements SDK.KAS, AutoCloseable {

private final Function<SDK.KASInfo, Channel> channelFactory;
private final Function<String, ManagedChannel> channelFactory;
private final RSASSASigner signer;
private final AsymDecryption decryptor;
private final String publicKeyPEM;

public KASClient(Function <SDK.KASInfo, Channel> channelFactory) {
/***
* A client that communicates with KAS
* @param channelFactory A function that produces channels that can be used to communicate
* @param dpopKey
*/
public KASClient(Function <String, ManagedChannel> channelFactory, RSAKey dpopKey) {
this.channelFactory = channelFactory;
try {
this.signer = new RSASSASigner(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());
}

@Override
public String getPublicKey(Config.KASInfo kasInfo) {
return getStub(kasInfo.URL)
.publicKey(PublicKeyRequest.getDefaultInstance())
.getPublicKey();
}

@Override
public String getPublicKey(SDK.KASInfo kasInfo) {
return getStub(kasInfo).publicKey(PublicKeyRequest.getDefaultInstance()).getPublicKey();
public void close() {
var entries = new ArrayList<>(stubs.values());
stubs.clear();
for (var entry: entries) {
entry.channel.shutdownNow();
}
}

static class RewrapRequestBody {
String policy;
String clientPublicKey;
Manifest.KeyAccess keyAccess;
}

private static final Gson gson = new Gson();

@Override
public byte[] unwrap(SDK.KASInfo kasInfo, SDK.Policy policy) {
// this is obviously wrong. we still have to generate a correct request and decrypt the payload
return getStub(kasInfo).rewrap(RewrapRequest.getDefaultInstance()).getEntityWrappedKey().toByteArray();
public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) {
RewrapRequestBody body = new RewrapRequestBody();
body.policy = policy;
body.clientPublicKey = publicKeyPEM;
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();
return decryptor.decrypt(wrappedKey);
}

private final HashMap<SDK.KASInfo, AccessServiceGrpc.AccessServiceBlockingStub> stubs = new HashMap<>();
private final HashMap<String, CacheEntry> stubs = new HashMap<>();
private static class CacheEntry {
final ManagedChannel channel;
final AccessServiceGrpc.AccessServiceBlockingStub stub;

private synchronized AccessServiceGrpc.AccessServiceBlockingStub getStub(SDK.KASInfo kasInfo) {
if (!stubs.containsKey(kasInfo)) {
var channel = channelFactory.apply(kasInfo);
private CacheEntry(ManagedChannel channel, AccessServiceGrpc.AccessServiceBlockingStub stub) {
this.channel = channel;
this.stub = stub;
}
}

private synchronized AccessServiceGrpc.AccessServiceBlockingStub getStub(String url) {
if (!stubs.containsKey(url)) {
var channel = channelFactory.apply(url);
var stub = AccessServiceGrpc.newBlockingStub(channel);
stubs.put(kasInfo, stub);
stubs.put(url, new CacheEntry(channel, stub));
}

return stubs.get(kasInfo);
return stubs.get(url).stub;
}
}

7 changes: 2 additions & 5 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@
public class SDK {
private final Services services;

public interface KASInfo{
String getAddress();
}
public interface Policy{}

interface KAS {
String getPublicKey(KASInfo kasInfo);
byte[] unwrap(KASInfo kasInfo, Policy policy);
String getPublicKey(Config.KASInfo kasInfo);
byte[] unwrap(Manifest.KeyAccess keyAccess, String policy);
}

// TODO: add KAS
Expand Down
Loading
Loading