Skip to content

Commit aafd8ee

Browse files
authored
Merge pull request #15 from justplaz/partial-kp
Adds PartialKeyPair for supporting encrypt/decrypt only behavior
2 parents 0cd3f8b + 5d455fe commit aafd8ee

File tree

8 files changed

+363
-18
lines changed

8 files changed

+363
-18
lines changed

pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@
142142
</execution>
143143
</executions>
144144
</plugin>
145+
<plugin>
146+
<groupId>org.apache.maven.plugins</groupId>
147+
<artifactId>maven-checkstyle-plugin</artifactId>
148+
<version>3.2.0</version>
149+
<configuration>
150+
<configLocation>utils/checkstyle.xml</configLocation>
151+
</configuration>
152+
<executions>
153+
<execution>
154+
<goals>
155+
<goal>check</goal>
156+
</goals>
157+
</execution>
158+
</executions>
159+
</plugin>
145160
</plugins>
146161
</build>
147162

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
2323
import software.amazon.encryption.s3.materials.Keyring;
2424
import software.amazon.encryption.s3.materials.KmsKeyring;
25+
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
2526
import software.amazon.encryption.s3.materials.RsaKeyring;
2627

2728
/**
@@ -94,7 +95,7 @@ public static class Builder {
9495
private CryptographicMaterialsManager _cryptoMaterialsManager;
9596
private Keyring _keyring;
9697
private SecretKey _aesKey;
97-
private KeyPair _rsaKeyPair;
98+
private PartialRsaKeyPair _rsaKeyPair;
9899
private String _kmsKeyId;
99100
private boolean _enableLegacyModes = false;
100101

@@ -127,7 +128,14 @@ public Builder aesKey(SecretKey aesKey) {
127128
}
128129

129130
public Builder rsaKeyPair(KeyPair rsaKeyPair) {
130-
this._rsaKeyPair = rsaKeyPair;
131+
this._rsaKeyPair = new PartialRsaKeyPair(rsaKeyPair);
132+
checkKeyOptions();
133+
134+
return this;
135+
}
136+
137+
public Builder rsaKeyPair(PartialRsaKeyPair partialRsaKeyPair) {
138+
this._rsaKeyPair = partialRsaKeyPair;
131139
checkKeyOptions();
132140

133141
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
@@ -3,7 +3,6 @@
33
import java.nio.charset.StandardCharsets;
44
import java.security.GeneralSecurityException;
55
import java.security.Key;
6-
import java.security.KeyPair;
76
import java.security.SecureRandom;
87
import java.security.spec.MGF1ParameterSpec;
98
import java.util.Arrays;
@@ -21,9 +20,7 @@
2120
*/
2221
public class RsaKeyring extends S3Keyring {
2322

24-
private static final String KEY_ALGORITHM = "RSA";
25-
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

@@ -74,9 +71,9 @@ public String keyProviderInfo() {
7471

7572
@Override
7673
public byte[] encryptDataKey(SecureRandom secureRandom,
77-
EncryptionMaterials materials) throws GeneralSecurityException {
74+
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+
}

0 commit comments

Comments
 (0)