Skip to content

Commit c7120e1

Browse files
authored
feat: Adds Async client, starting with DeleteObject(s) (#54)
* adds async client * implements DeleteObject(s) in async client * removes a stray comment in UnauthenticatedCipherStream
1 parent ef8effb commit c7120e1

File tree

7 files changed

+448
-20
lines changed

7 files changed

+448
-20
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package software.amazon.encryption.s3;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
5+
import software.amazon.awssdk.awscore.exception.AwsServiceException;
6+
import software.amazon.awssdk.core.exception.SdkClientException;
7+
import software.amazon.awssdk.core.interceptor.ExecutionAttribute;
8+
import software.amazon.awssdk.services.s3.S3AsyncClient;
9+
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
10+
import software.amazon.awssdk.services.s3.model.DeleteObjectResponse;
11+
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
12+
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
13+
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
14+
import software.amazon.encryption.s3.materials.AesKeyring;
15+
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
16+
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
17+
import software.amazon.encryption.s3.materials.Keyring;
18+
import software.amazon.encryption.s3.materials.KmsKeyring;
19+
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
20+
import software.amazon.encryption.s3.materials.RsaKeyring;
21+
22+
import javax.crypto.SecretKey;
23+
import java.security.KeyPair;
24+
import java.security.Provider;
25+
import java.security.SecureRandom;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.concurrent.CompletableFuture;
29+
import java.util.function.Consumer;
30+
31+
public class S3AsyncEncryptionClient implements S3AsyncClient {
32+
33+
// Used for request-scoped encryption contexts for supporting keys
34+
public static final ExecutionAttribute<Map<String, String>> ENCRYPTION_CONTEXT_ASYNC = new ExecutionAttribute<>("EncryptionContextAsync");
35+
36+
private final S3AsyncClient _wrappedClient;
37+
private final CryptographicMaterialsManager _cryptoMaterialsManager;
38+
private final SecureRandom _secureRandom;
39+
private final boolean _enableLegacyUnauthenticatedModes;
40+
private final boolean _enableDelayedAuthenticationMode;
41+
42+
private S3AsyncEncryptionClient(Builder builder) {
43+
_wrappedClient = builder._wrappedClient;
44+
_cryptoMaterialsManager = builder._cryptoMaterialsManager;
45+
_secureRandom = builder._secureRandom;
46+
_enableLegacyUnauthenticatedModes = builder._enableLegacyUnauthenticatedModes;
47+
_enableDelayedAuthenticationMode = builder._enableDelayedAuthenticationMode;
48+
}
49+
50+
public static Builder builder() {
51+
return new Builder();
52+
}
53+
54+
// Helper function to attach encryption contexts to a request
55+
public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalEncryptionContext(Map<String, String> encryptionContext) {
56+
return builder ->
57+
builder.putExecutionAttribute(S3AsyncEncryptionClient.ENCRYPTION_CONTEXT_ASYNC, encryptionContext);
58+
}
59+
60+
@Override
61+
public CompletableFuture<DeleteObjectResponse> deleteObject(DeleteObjectRequest deleteObjectRequest) {
62+
// TODO: Pass-through requests MUST set the user agent
63+
final CompletableFuture<DeleteObjectResponse> response = _wrappedClient.deleteObject(deleteObjectRequest);
64+
final String instructionObjectKey = deleteObjectRequest.key() + ".instruction";
65+
// Deleting the instruction file is "fire and forget"
66+
// This is necessary because the encryption client must adhere to the
67+
// same interface as the default client thus it is not possible to
68+
// use e.g. allOf to return a future which includes both deletions.
69+
_wrappedClient.deleteObject(builder -> builder
70+
.bucket(deleteObjectRequest.bucket())
71+
.key(instructionObjectKey));
72+
return response;
73+
}
74+
75+
@Override
76+
public CompletableFuture<DeleteObjectsResponse> deleteObjects(DeleteObjectsRequest deleteObjectsRequest) throws AwsServiceException,
77+
SdkClientException {
78+
// TODO: Pass-through requests MUST set the user agent
79+
final CompletableFuture<DeleteObjectsResponse> deleteObjectsResponse = _wrappedClient.deleteObjects(deleteObjectsRequest);
80+
// If Instruction files exists, delete the instruction files as well.
81+
final List<ObjectIdentifier> deleteObjects = S3EncryptionClientUtilities.instructionFileKeysToDelete(deleteObjectsRequest);
82+
// Deleting the instruction files is "fire and forget"
83+
// This is necessary because the encryption client must adhere to the
84+
// same interface as the default client thus it is not possible to
85+
// use e.g. allOf to return a future which includes both deletions.
86+
_wrappedClient.deleteObjects(DeleteObjectsRequest.builder()
87+
.bucket(deleteObjectsRequest.bucket())
88+
.delete(builder -> builder.objects(deleteObjects))
89+
.build());
90+
return deleteObjectsResponse;
91+
}
92+
93+
@Override
94+
public String serviceName() {
95+
return _wrappedClient.serviceName();
96+
}
97+
98+
@Override
99+
public void close() {
100+
_wrappedClient.close();
101+
}
102+
103+
// TODO: The async / non-async clients can probably share a builder - revisit after implementing async
104+
public static class Builder {
105+
private S3AsyncClient _wrappedClient = S3AsyncClient.builder().build();
106+
private CryptographicMaterialsManager _cryptoMaterialsManager;
107+
private Keyring _keyring;
108+
private SecretKey _aesKey;
109+
private PartialRsaKeyPair _rsaKeyPair;
110+
private String _kmsKeyId;
111+
private boolean _enableLegacyUnauthenticatedModes = false;
112+
private boolean _enableDelayedAuthenticationMode = false;
113+
private Provider _cryptoProvider = null;
114+
private SecureRandom _secureRandom = new SecureRandom();
115+
116+
private Builder() {
117+
}
118+
119+
/**
120+
* Note that this does NOT create a defensive clone of S3Client. Any modifications made to the wrapped
121+
* S3Client will be reflected in this Builder.
122+
*/
123+
@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Pass mutability into wrapping client")
124+
public Builder wrappedClient(S3AsyncClient wrappedClient) {
125+
if (wrappedClient instanceof S3AsyncEncryptionClient) {
126+
throw new S3EncryptionClientException("Cannot use S3EncryptionClient as wrapped client");
127+
}
128+
129+
this._wrappedClient = wrappedClient;
130+
return this;
131+
}
132+
133+
public Builder cryptoMaterialsManager(CryptographicMaterialsManager cryptoMaterialsManager) {
134+
this._cryptoMaterialsManager = cryptoMaterialsManager;
135+
checkKeyOptions();
136+
137+
return this;
138+
}
139+
140+
public Builder keyring(Keyring keyring) {
141+
this._keyring = keyring;
142+
checkKeyOptions();
143+
144+
return this;
145+
}
146+
147+
public Builder aesKey(SecretKey aesKey) {
148+
this._aesKey = aesKey;
149+
checkKeyOptions();
150+
151+
return this;
152+
}
153+
154+
public Builder rsaKeyPair(KeyPair rsaKeyPair) {
155+
this._rsaKeyPair = new PartialRsaKeyPair(rsaKeyPair);
156+
checkKeyOptions();
157+
158+
return this;
159+
}
160+
161+
public Builder rsaKeyPair(PartialRsaKeyPair partialRsaKeyPair) {
162+
this._rsaKeyPair = partialRsaKeyPair;
163+
checkKeyOptions();
164+
165+
return this;
166+
}
167+
168+
public Builder kmsKeyId(String kmsKeyId) {
169+
this._kmsKeyId = kmsKeyId;
170+
checkKeyOptions();
171+
172+
return this;
173+
}
174+
175+
// We only want one way to use a key, if more than one is set, throw an error
176+
private void checkKeyOptions() {
177+
if (onlyOneNonNull(_cryptoMaterialsManager, _keyring, _aesKey, _rsaKeyPair, _kmsKeyId)) {
178+
return;
179+
}
180+
181+
throw new S3EncryptionClientException("Only one may be set of: crypto materials manager, keyring, AES key, RSA key pair, KMS key id");
182+
}
183+
184+
private boolean onlyOneNonNull(Object... values) {
185+
boolean haveOneNonNull = false;
186+
for (Object o : values) {
187+
if (o != null) {
188+
if (haveOneNonNull) {
189+
return false;
190+
}
191+
192+
haveOneNonNull = true;
193+
}
194+
}
195+
196+
return haveOneNonNull;
197+
}
198+
199+
public Builder enableLegacyUnauthenticatedModes(boolean shouldEnableLegacyUnauthenticatedModes) {
200+
this._enableLegacyUnauthenticatedModes = shouldEnableLegacyUnauthenticatedModes;
201+
return this;
202+
}
203+
204+
public Builder enableDelayedAuthenticationMode(boolean shouldEnableDelayedAuthenticationMode) {
205+
this._enableDelayedAuthenticationMode = shouldEnableDelayedAuthenticationMode;
206+
return this;
207+
}
208+
209+
public Builder cryptoProvider(Provider cryptoProvider) {
210+
this._cryptoProvider = cryptoProvider;
211+
return this;
212+
}
213+
214+
public Builder secureRandom(SecureRandom secureRandom) {
215+
if (secureRandom == null) {
216+
throw new S3EncryptionClientException("SecureRandom provided to S3EncryptionClient cannot be null");
217+
}
218+
_secureRandom = secureRandom;
219+
return this;
220+
}
221+
222+
public S3AsyncEncryptionClient build() {
223+
if (!onlyOneNonNull(_cryptoMaterialsManager, _keyring, _aesKey, _rsaKeyPair, _kmsKeyId)) {
224+
throw new S3EncryptionClientException("Exactly one must be set of: crypto materials manager, keyring, AES key, RSA key pair, KMS key id");
225+
}
226+
227+
if (_keyring == null) {
228+
if (_aesKey != null) {
229+
_keyring = AesKeyring.builder()
230+
.wrappingKey(_aesKey)
231+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
232+
.secureRandom(_secureRandom)
233+
.build();
234+
} else if (_rsaKeyPair != null) {
235+
_keyring = RsaKeyring.builder()
236+
.wrappingKeyPair(_rsaKeyPair)
237+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
238+
.secureRandom(_secureRandom)
239+
.build();
240+
} else if (_kmsKeyId != null) {
241+
_keyring = KmsKeyring.builder()
242+
.wrappingKeyId(_kmsKeyId)
243+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
244+
.secureRandom(_secureRandom)
245+
.build();
246+
}
247+
}
248+
249+
if (_cryptoMaterialsManager == null) {
250+
_cryptoMaterialsManager = DefaultCryptoMaterialsManager.builder()
251+
.keyring(_keyring)
252+
.cryptoProvider(_cryptoProvider)
253+
.build();
254+
}
255+
256+
return new S3AsyncEncryptionClient(this);
257+
}
258+
}
259+
}

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

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

33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4-
5-
import java.security.KeyPair;
6-
import java.security.Provider;
7-
import java.util.ArrayList;
8-
import java.util.List;
9-
import java.security.SecureRandom;
10-
import java.util.Map;
11-
import java.util.function.Consumer;
12-
import javax.crypto.SecretKey;
13-
144
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
155
import software.amazon.awssdk.awscore.exception.AwsServiceException;
166
import software.amazon.awssdk.core.exception.SdkClientException;
@@ -37,6 +27,14 @@
3727
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
3828
import software.amazon.encryption.s3.materials.RsaKeyring;
3929

30+
import javax.crypto.SecretKey;
31+
import java.security.KeyPair;
32+
import java.security.Provider;
33+
import java.security.SecureRandom;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.function.Consumer;
37+
4038
/**
4139
* This client is a drop-in replacement for the S3 client. It will automatically encrypt objects
4240
* on putObject and decrypt objects on getObject using the provided encryption key(s).
@@ -117,12 +115,7 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq
117115
// Delete the objects
118116
DeleteObjectsResponse deleteObjectsResponse = _wrappedClient.deleteObjects(deleteObjectsRequest);
119117
// If Instruction files exists, delete the instruction files as well.
120-
List<ObjectIdentifier> deleteObjects = new ArrayList<>();
121-
for (ObjectIdentifier o : deleteObjectsRequest.delete().objects()) {
122-
deleteObjects.add(o.toBuilder()
123-
.key(o.key() + ".instruction")
124-
.build());
125-
}
118+
List<ObjectIdentifier> deleteObjects = S3EncryptionClientUtilities.instructionFileKeysToDelete(deleteObjectsRequest);
126119
_wrappedClient.deleteObjects(DeleteObjectsRequest.builder()
127120
.bucket(deleteObjectsRequest.bucket())
128121
.delete(builder -> builder.objects(deleteObjects))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package software.amazon.encryption.s3;
2+
3+
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
4+
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
5+
6+
import java.util.List;
7+
import java.util.stream.Collectors;
8+
9+
/**
10+
* This class contains that which can be shared between the default S3 Encryption
11+
* Client and its Async counterpart.
12+
* TODO: move encryption context handling here
13+
* TODO: move the builder here
14+
*/
15+
public class S3EncryptionClientUtilities {
16+
17+
public static final String INSTRUCTION_FILE_SUFFIX = ".instruction";
18+
19+
/**
20+
* For a given DeleteObjectsRequest, return a list of ObjectIdentifiers
21+
* representing the corresponding instruction files to delete.
22+
* @param request a DeleteObjectsRequest
23+
* @return the list of ObjectIdentifiers for instruction files to delete
24+
*/
25+
static List<ObjectIdentifier> instructionFileKeysToDelete(final DeleteObjectsRequest request) {
26+
return request.delete().objects().stream()
27+
.map(o -> o.toBuilder().key(o.key() + INSTRUCTION_FILE_SUFFIX).build())
28+
.collect(Collectors.toList());
29+
}
30+
}

src/main/java/software/amazon/encryption/s3/internal/ContentMetadataStrategy.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@
2121
import java.util.Map;
2222
import java.util.Map.Entry;
2323

24+
import static software.amazon.encryption.s3.S3EncryptionClientUtilities.INSTRUCTION_FILE_SUFFIX;
25+
2426
public abstract class ContentMetadataStrategy implements ContentMetadataEncodingStrategy, ContentMetadataDecodingStrategy {
2527

2628
private static final Base64.Encoder ENCODER = Base64.getEncoder();
2729
private static final Base64.Decoder DECODER = Base64.getDecoder();
2830

2931
public static final ContentMetadataDecodingStrategy INSTRUCTION_FILE = new ContentMetadataDecodingStrategy() {
3032

31-
private static final String FILE_SUFFIX = ".instruction";
32-
3333
@Override
3434
public ContentMetadata decodeMetadata(S3Client client, GetObjectRequest getObjectRequest, GetObjectResponse response) {
3535
GetObjectRequest instructionGetObjectRequest = GetObjectRequest.builder()
3636
.bucket(getObjectRequest.bucket())
37-
.key(getObjectRequest.key() + FILE_SUFFIX)
37+
.key(getObjectRequest.key() + INSTRUCTION_FILE_SUFFIX)
3838
.build();
3939
ResponseInputStream<GetObjectResponse> instruction = client.getObject(
4040
instructionGetObjectRequest);

src/main/java/software/amazon/encryption/s3/legacy/internal/UnauthenticatedContentStrategy.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMat
3939
}
4040
SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), algorithmSuite.dataKeyAlgorithm());
4141
try {
42-
// TODO: Allow configurable Cryptographic provider
4342
final Cipher cipher = CryptoFactory.createCipher(algorithmSuite.cipherName(), materials.cryptoProvider());
4443
cipher.init(Cipher.DECRYPT_MODE, contentKey, new IvParameterSpec(iv));
4544
InputStream plaintext = new CipherInputStream(ciphertextStream, cipher);

0 commit comments

Comments
 (0)