Skip to content

Commit 37a8479

Browse files
author
J Plasmeier
committed
Adds PartialKeyPair for supporting encrypt/decrypt only behavior, implements PartialRsaKeyPair for use in RsaKeyring
1 parent 136b025 commit 37a8479

File tree

7 files changed

+323
-25
lines changed

7 files changed

+323
-25
lines changed

src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package software.amazon.encryption.s3;
22

3-
import java.security.KeyPair;
3+
import java.security.*;
44
import java.util.Map;
55
import java.util.function.Consumer;
66
import javax.crypto.SecretKey;
@@ -17,12 +17,7 @@
1717
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
1818
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
1919
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
20-
import software.amazon.encryption.s3.materials.AesKeyring;
21-
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
22-
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
23-
import software.amazon.encryption.s3.materials.Keyring;
24-
import software.amazon.encryption.s3.materials.KmsKeyring;
25-
import software.amazon.encryption.s3.materials.RsaKeyring;
20+
import software.amazon.encryption.s3.materials.*;
2621

2722
/**
2823
* This client is a drop-in replacement for the S3 client. It will automatically encrypt objects
@@ -94,7 +89,7 @@ public static class Builder {
9489
private CryptographicMaterialsManager _cryptoMaterialsManager;
9590
private Keyring _keyring;
9691
private SecretKey _aesKey;
97-
private KeyPair _rsaKeyPair;
92+
private PartialRsaKeyPair _rsaKeyPair;
9893
private String _kmsKeyId;
9994
private boolean _enableLegacyModes = false;
10095

@@ -127,7 +122,14 @@ public Builder aesKey(SecretKey aesKey) {
127122
}
128123

129124
public Builder rsaKeyPair(KeyPair rsaKeyPair) {
130-
this._rsaKeyPair = rsaKeyPair;
125+
this._rsaKeyPair = new PartialRsaKeyPair(rsaKeyPair);
126+
checkKeyOptions();
127+
128+
return this;
129+
}
130+
131+
public Builder rsaKeyPair(PartialRsaKeyPair partialRsaKeyPair) {
132+
this._rsaKeyPair = partialRsaKeyPair;
131133
checkKeyOptions();
132134

133135
return this;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package software.amazon.encryption.s3.materials;
2+
3+
import java.security.PrivateKey;
4+
import java.security.PublicKey;
5+
6+
/**
7+
* This interface allows use of key pairs where only one of the public or private keys
8+
* has been provided. This allows consumers to be able to e.g. provide only the
9+
* public portion of a key pair in the part of their application which puts encrypted
10+
* objects into S3 to avoid distributing the private key.
11+
*/
12+
public interface PartialKeyPair {
13+
PublicKey getPublicKey();
14+
15+
PrivateKey getPrivateKey();
16+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package software.amazon.encryption.s3.materials;
2+
3+
import software.amazon.encryption.s3.S3EncryptionClientException;
4+
5+
import java.security.KeyPair;
6+
import java.security.PrivateKey;
7+
import java.security.PublicKey;
8+
import java.util.Objects;
9+
10+
public class PartialRsaKeyPair implements PartialKeyPair {
11+
private final PrivateKey _privateKey;
12+
private final PublicKey _publicKey;
13+
14+
private static final String RSA_KEY_ALGORITHM = "RSA";
15+
16+
public PartialRsaKeyPair(final KeyPair keyPair) {
17+
_privateKey = keyPair.getPrivate();
18+
_publicKey = keyPair.getPublic();
19+
20+
validateKeyPair();
21+
}
22+
23+
public PartialRsaKeyPair(final PrivateKey privateKey, final PublicKey publicKey) {
24+
_privateKey = privateKey;
25+
_publicKey = publicKey;
26+
27+
validateKeyPair();
28+
}
29+
30+
private void validateKeyPair() {
31+
if (_privateKey == null && _publicKey == null) {
32+
throw new S3EncryptionClientException("The public key and private cannot both be null. You must provide a " +
33+
"public key, or a private key, or both.");
34+
}
35+
36+
if (_privateKey != null && !_privateKey.getAlgorithm().equals(RSA_KEY_ALGORITHM)) {
37+
throw new S3EncryptionClientException("%s is not a supported algorithm. Only RSA keys are supported. Please reconfigure your client with an RSA key.");
38+
}
39+
40+
if (_publicKey != null && !_publicKey.getAlgorithm().equals(RSA_KEY_ALGORITHM)) {
41+
throw new S3EncryptionClientException("%s is not a supported algorithm. Only RSA keys are supported. Please reconfigure your client with an RSA key.");
42+
}
43+
}
44+
45+
@Override
46+
public PublicKey getPublicKey() {
47+
if (_publicKey == null) {
48+
throw new S3EncryptionClientException("No public key provided. You must configure a public key to be able to" +
49+
" encrypt data.");
50+
}
51+
return _publicKey;
52+
}
53+
54+
@Override
55+
public PrivateKey getPrivateKey() {
56+
if (_privateKey == null) {
57+
throw new S3EncryptionClientException("No private key provided. You must configure a private key to be able to" +
58+
" decrypt data.");
59+
}
60+
return _privateKey;
61+
}
62+
63+
public static Builder builder() {
64+
return new Builder();
65+
}
66+
67+
@Override
68+
public boolean equals(Object o) {
69+
if (o == null || getClass() != o.getClass()) return false;
70+
PartialRsaKeyPair that = (PartialRsaKeyPair) o;
71+
return Objects.equals(_privateKey, that._privateKey) && Objects.equals(_publicKey, that._publicKey);
72+
}
73+
74+
@Override
75+
public int hashCode() {
76+
return Objects.hash(_privateKey, _publicKey);
77+
}
78+
79+
public static class Builder {
80+
private PublicKey _publicKey;
81+
private PrivateKey _privateKey;
82+
83+
private Builder() {}
84+
85+
public Builder publicKey(final PublicKey publicKey) {
86+
_publicKey = publicKey;
87+
return this;
88+
}
89+
90+
public Builder privateKey(final PrivateKey privateKey) {
91+
_privateKey = privateKey;
92+
return this;
93+
}
94+
95+
public PartialRsaKeyPair build() {
96+
return new PartialRsaKeyPair(_privateKey, _publicKey);
97+
}
98+
}
99+
}

src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package software.amazon.encryption.s3.materials;
22

33
import java.nio.charset.StandardCharsets;
4-
import java.security.GeneralSecurityException;
5-
import java.security.Key;
6-
import java.security.KeyPair;
7-
import java.security.SecureRandom;
4+
import java.security.*;
85
import java.security.spec.MGF1ParameterSpec;
96
import java.util.Arrays;
107
import java.util.HashMap;
@@ -23,7 +20,7 @@ public class RsaKeyring extends S3Keyring {
2320

2421
private static final String KEY_ALGORITHM = "RSA";
2522

26-
private final KeyPair _wrappingKeyPair;
23+
private final PartialRsaKeyPair _partialRsaKeyPair;
2724

2825
private final DecryptDataKeyStrategy _rsaEcbStrategy = new DecryptDataKeyStrategy() {
2926
private static final String KEY_PROVIDER_INFO = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
@@ -42,7 +39,7 @@ public String keyProviderInfo() {
4239
@Override
4340
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
4441
final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
45-
cipher.init(Cipher.UNWRAP_MODE, _wrappingKeyPair.getPrivate());
42+
cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey());
4643

4744
Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY);
4845

@@ -76,7 +73,7 @@ public String keyProviderInfo() {
7673
public byte[] encryptDataKey(SecureRandom secureRandom,
7774
EncryptionMaterials materials) throws GeneralSecurityException {
7875
final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
79-
cipher.init(Cipher.WRAP_MODE, _wrappingKeyPair.getPublic(), OAEP_PARAMETER_SPEC, secureRandom);
76+
cipher.init(Cipher.WRAP_MODE, _partialRsaKeyPair.getPublicKey(), OAEP_PARAMETER_SPEC, secureRandom);
8077

8178
// Create a pseudo-data key with the content encryption appended to the data key
8279
byte[] dataKey = materials.plaintextDataKey();
@@ -95,7 +92,7 @@ public byte[] encryptDataKey(SecureRandom secureRandom,
9592
@Override
9693
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
9794
final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
98-
cipher.init(Cipher.UNWRAP_MODE, _wrappingKeyPair.getPrivate(), OAEP_PARAMETER_SPEC);
95+
cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey(), OAEP_PARAMETER_SPEC);
9996

10097
String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm();
10198
Key pseudoDataKey = cipher.unwrap(encryptedDataKey, dataKeyAlgorithm, Cipher.SECRET_KEY);
@@ -133,7 +130,7 @@ private byte[] parsePseudoDataKey(DecryptionMaterials materials, byte[] pseudoDa
133130
private RsaKeyring(Builder builder) {
134131
super(builder);
135132

136-
_wrappingKeyPair = builder._wrappingKeyPair;
133+
_partialRsaKeyPair = builder._partialRsaKeyPair;
137134

138135
decryptStrategies.put(_rsaEcbStrategy.keyProviderInfo(), _rsaEcbStrategy);
139136
decryptStrategies.put(_rsaOaepStrategy.keyProviderInfo(), _rsaOaepStrategy);
@@ -143,7 +140,6 @@ public static Builder builder() {
143140
return new Builder();
144141
}
145142

146-
147143
@Override
148144
protected EncryptDataKeyStrategy encryptStrategy() {
149145
return _rsaOaepStrategy;
@@ -155,7 +151,7 @@ protected Map<String, DecryptDataKeyStrategy> decryptStrategies() {
155151
}
156152

157153
public static class Builder extends S3Keyring.Builder<S3Keyring, Builder> {
158-
private KeyPair _wrappingKeyPair;
154+
private PartialRsaKeyPair _partialRsaKeyPair;
159155

160156
private Builder() {
161157
super();
@@ -166,11 +162,8 @@ protected Builder builder() {
166162
return this;
167163
}
168164

169-
public Builder wrappingKeyPair(KeyPair wrappingKeyPair) {
170-
if (!wrappingKeyPair.getPublic().getAlgorithm().equals(KEY_ALGORITHM)) {
171-
throw new S3EncryptionClientException("Invalid algorithm '" + wrappingKeyPair.getPublic().getAlgorithm() + "', expecting " + KEY_ALGORITHM);
172-
}
173-
_wrappingKeyPair = wrappingKeyPair;
165+
public Builder wrappingKeyPair(final PartialRsaKeyPair partialRsaKeyPair) {
166+
_partialRsaKeyPair = partialRsaKeyPair;
174167
return builder();
175168
}
176169

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package software.amazon.encryption.s3;
2+
3+
import org.junit.jupiter.api.BeforeAll;
4+
import org.junit.jupiter.api.Test;
5+
import software.amazon.awssdk.core.ResponseBytes;
6+
import software.amazon.awssdk.core.sync.RequestBody;
7+
import software.amazon.awssdk.services.s3.S3Client;
8+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
9+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
10+
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
11+
12+
import java.security.KeyPair;
13+
import java.security.KeyPairGenerator;
14+
import java.security.NoSuchAlgorithmException;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertThrows;
18+
19+
public class S3EncryptionClientRsaKeyPairTest {
20+
private static final String BUCKET = System.getenv("AWS_S3EC_TEST_BUCKET");
21+
22+
private static KeyPair RSA_KEY_PAIR;
23+
24+
@BeforeAll
25+
public static void setUp() throws NoSuchAlgorithmException {
26+
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
27+
keyPairGen.initialize(2048);
28+
RSA_KEY_PAIR = keyPairGen.generateKeyPair();
29+
}
30+
31+
@Test
32+
public void RsaPublicAndPrivateKeys() {
33+
final String BUCKET_KEY = "rsa-public-and-private";
34+
35+
// V3 Client
36+
S3Client v3Client = S3EncryptionClient.builder()
37+
.rsaKeyPair(RSA_KEY_PAIR)
38+
.build();
39+
40+
// Asserts
41+
final String input = "RsaOaepV3toV3";
42+
v3Client.putObject(PutObjectRequest.builder()
43+
.bucket(BUCKET)
44+
.key(BUCKET_KEY)
45+
.build(), RequestBody.fromString(input));
46+
47+
ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(builder -> builder
48+
.bucket(BUCKET)
49+
.key(BUCKET_KEY));
50+
String output = objectResponse.asUtf8String();
51+
assertEquals(input, output);
52+
}
53+
54+
@Test
55+
public void RsaPrivateKeyCanOnlyDecrypt() {
56+
S3Client v3Client = S3EncryptionClient.builder()
57+
.rsaKeyPair(RSA_KEY_PAIR)
58+
.build();
59+
60+
S3Client v3ClientReadOnly = S3EncryptionClient.builder()
61+
.rsaKeyPair(new PartialRsaKeyPair(RSA_KEY_PAIR.getPrivate(), null))
62+
.build();
63+
64+
final String input = "RsaOaepV3toV3";
65+
v3Client.putObject(PutObjectRequest.builder()
66+
.bucket(BUCKET)
67+
.key(input)
68+
.build(), RequestBody.fromString(input));
69+
70+
ResponseBytes<GetObjectResponse> objectResponse = v3ClientReadOnly.getObjectAsBytes(builder -> builder
71+
.bucket(BUCKET)
72+
.key(input));
73+
String output = objectResponse.asUtf8String();
74+
assertEquals(input, output);
75+
76+
assertThrows(S3EncryptionClientException.class, () -> v3ClientReadOnly.putObject(PutObjectRequest.builder()
77+
.bucket(BUCKET)
78+
.key(input)
79+
.build(), RequestBody.fromString(input)));
80+
}
81+
82+
@Test
83+
public void RsaPublicKeyCanOnlyEncrypt() {
84+
final String BUCKET_KEY = "rsa-public-key-only";
85+
S3Client v3Client = S3EncryptionClient.builder()
86+
.rsaKeyPair(new PartialRsaKeyPair(null, RSA_KEY_PAIR.getPublic()))
87+
.build();
88+
89+
v3Client.putObject(PutObjectRequest.builder()
90+
.bucket(BUCKET)
91+
.key(BUCKET_KEY)
92+
.build(), RequestBody.fromString(BUCKET_KEY));
93+
94+
assertThrows(S3EncryptionClientException.class, () -> v3Client.getObjectAsBytes(builder -> builder
95+
.bucket(BUCKET)
96+
.key(BUCKET_KEY)));
97+
}
98+
99+
100+
}

src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import software.amazon.awssdk.core.sync.RequestBody;
66
import software.amazon.awssdk.services.s3.S3Client;
77
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
8+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
89

910
import javax.crypto.KeyGenerator;
1011
import javax.crypto.SecretKey;

0 commit comments

Comments
 (0)