Skip to content

Commit 60feafd

Browse files
committed
Merge branch 'main' of github.com:aws/amazon-s3-encryption-client-java into inst-file-put
2 parents d6da2cf + 2db1784 commit 60feafd

12 files changed

+574
-61
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## [3.3.5](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.3.4...v3.3.5) (2025-05-21)
4+
5+
### Fixes
6+
7+
* determine effective contentLength, account for tagLength on decrypt ([#463](https://github.com/aws/aws-s3-encryption-client-java/issues/463)) ([969d721](https://github.com/aws/aws-s3-encryption-client-java/commit/969d7213b7bd6250fbce159bd5705a19ee439f23))
8+
* disable low-level Multipart Upload in Async client ([#461](https://github.com/aws/aws-s3-encryption-client-java/issues/461)) ([599f941](https://github.com/aws/aws-s3-encryption-client-java/commit/599f9417335efac4cf952e555a99bbc6d7d8cc0f))
9+
* support PutObjectResponse fields ([#462](https://github.com/aws/aws-s3-encryption-client-java/issues/462)) ([dec503b](https://github.com/aws/aws-s3-encryption-client-java/commit/dec503b49f3e57113de16fcc861ee1ecedbd2ff6))
10+
11+
### Maintenance
12+
13+
* Revert "Amazon S3 Encryption Client 3.3.5 Release -- 2025-05-20" ([#465](https://github.com/aws/aws-s3-encryption-client-java/issues/465)) ([3f9ac8e](https://github.com/aws/aws-s3-encryption-client-java/commit/3f9ac8e419f5ed55f59fb0daf742fc2945eea9d0))
14+
* update dependency needed for semantic-release ([#464](https://github.com/aws/aws-s3-encryption-client-java/issues/464)) ([0fd3b58](https://github.com/aws/aws-s3-encryption-client-java/commit/0fd3b5826964b41a07a4c004fd0500404e347360))
15+
16+
## [3.3.4](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.3.3...v3.3.4) (2025-05-12)
17+
18+
### Fixes
19+
20+
* Add details to error message ([#459](https://github.com/aws/aws-s3-encryption-client-java/issues/459)) ([0d32b4a](https://github.com/aws/aws-s3-encryption-client-java/commit/0d32b4a81662c5dc8ca85cf6f5dd09252f014b6e)), closes [#458](https://github.com/aws/aws-s3-encryption-client-java/issues/458)
21+
* Support all PutObjectRequest fields ([#458](https://github.com/aws/aws-s3-encryption-client-java/issues/458)) ([99cce95](https://github.com/aws/aws-s3-encryption-client-java/commit/99cce95ab8e376f4a096401e56a88d6fc34ace44))
22+
323
## [3.3.3](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.3.2...v3.3.3) (2025-05-05)
424

525
### Fixes

codebuild/release/version.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ phases:
1919
- npm install @semantic-release/changelog -d
2020
- npm install @semantic-release/exec -d
2121
- npm install @semantic-release/git -d
22+
- npm install conventional-changelog-conventionalcommits -d
2223
- npm install --save conventional-changelog
2324
runtime-versions:
2425
nodejs: 16
@@ -30,4 +31,4 @@ phases:
3031
build:
3132
commands:
3233
# semantic-release uses config stored in ~/.releaserc
33-
- npx semantic-release --branches $BRANCH --no-ci
34+
- npx semantic-release --branches $BRANCH --no-ci

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>software.amazon.encryption.s3</groupId>
88
<artifactId>amazon-s3-encryption-client-java</artifactId>
9-
<version>3.3.3</version>
9+
<version>3.3.5</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Amazon S3 Encryption Client</name>

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import software.amazon.awssdk.services.s3.S3AsyncClientBuilder;
2121
import software.amazon.awssdk.services.s3.S3Configuration;
2222
import software.amazon.awssdk.services.s3.internal.crt.S3CrtAsyncClient;
23+
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
24+
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse;
25+
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
26+
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
2327
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
2428
import software.amazon.awssdk.services.s3.model.DeleteObjectResponse;
2529
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
@@ -30,6 +34,8 @@
3034
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
3135
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
3236
import software.amazon.awssdk.services.s3.model.S3Request;
37+
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
38+
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
3339
import software.amazon.awssdk.services.s3.multipart.MultipartConfiguration;
3440
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
3541
import software.amazon.encryption.s3.internal.InstructionFileConfig;
@@ -256,6 +262,24 @@ public CompletableFuture<DeleteObjectsResponse> deleteObjects(DeleteObjectsReque
256262
.build());
257263
}
258264

265+
@Override
266+
public CompletableFuture<CreateMultipartUploadResponse> createMultipartUpload(CreateMultipartUploadRequest createMultipartUploadRequest) {
267+
throw new UnsupportedOperationException("The S3 Async Encryption Client does not support low-level multipart uploads. " +
268+
"Please use Multipart PutObject or the default (synchronous) client to use this API.");
269+
}
270+
271+
@Override
272+
public CompletableFuture<UploadPartResponse> uploadPart(UploadPartRequest uploadPartRequest, AsyncRequestBody asyncRequestBody) {
273+
throw new UnsupportedOperationException("The S3 Async Encryption Client does not support low-level multipart uploads. " +
274+
"Please use Multipart PutObject or the default (synchronous) client to use this API.");
275+
}
276+
277+
@Override
278+
public CompletableFuture<CompleteMultipartUploadResponse> completeMultipartUpload(CompleteMultipartUploadRequest completeMultipartUploadRequest) {
279+
throw new UnsupportedOperationException("The S3 Async Encryption Client does not support low-level multipart uploads. " +
280+
"Please use Multipart PutObject or the default (synchronous) client to use this API.");
281+
}
282+
259283
/**
260284
* Closes the wrapped {@link S3AsyncClient} instance.
261285
*/

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
4747
import software.amazon.awssdk.services.s3.model.UploadPartResponse;
4848
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
49+
import software.amazon.encryption.s3.internal.ConvertSDKRequests;
4950
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
5051
import software.amazon.encryption.s3.internal.InstructionFileConfig;
5152
import software.amazon.encryption.s3.internal.MultiFileOutputStream;
@@ -192,11 +193,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod
192193

193194
if (_enableMultipartPutObject) {
194195
try {
195-
CompleteMultipartUploadResponse completeResponse = multipartPutObject(putObjectRequest, requestBody);
196-
PutObjectResponse response = PutObjectResponse.builder()
197-
.eTag(completeResponse.eTag())
198-
.build();
199-
return response;
196+
return multipartPutObject(putObjectRequest, requestBody);
200197
} catch (Throwable e) {
201198
throw new S3EncryptionClientException("Exception while performing Multipart Upload PutObject", e);
202199
}
@@ -275,7 +272,7 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
275272
}
276273
}
277274

278-
private CompleteMultipartUploadResponse multipartPutObject(PutObjectRequest request, RequestBody requestBody) throws Throwable {
275+
private PutObjectResponse multipartPutObject(PutObjectRequest request, RequestBody requestBody) throws Throwable {
279276
// Similar logic exists in the MultipartUploadObjectPipeline,
280277
// but the request types do not match so refactoring is not possible
281278
final long contentLength;
@@ -355,7 +352,7 @@ private CompleteMultipartUploadResponse multipartPutObject(PutObjectRequest requ
355352
outputStream.cleanup();
356353
}
357354
// Complete upload
358-
return observer.onCompletion(partETags);
355+
return ConvertSDKRequests.convertResponse(observer.onCompletion(partETags));
359356
}
360357

361358
private <T extends Throwable> T onAbort(UploadObjectObserver observer, T t) {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class CipherSubscriber implements Subscriber<ByteBuffer> {
2121
private final Long contentLength;
2222
private final boolean isLastPart;
2323
private final int tagLength;
24+
private final boolean isEncrypt;
2425
private final AtomicBoolean finalBytesCalled = new AtomicBoolean(false);
2526

2627
private byte[] outputBuffer;
@@ -31,6 +32,7 @@ public class CipherSubscriber implements Subscriber<ByteBuffer> {
3132
this.cipher = materials.getCipher(iv);
3233
this.isLastPart = isLastPart;
3334
this.tagLength = materials.algorithmSuite().cipherTagLengthBytes();
35+
this.isEncrypt = (CipherMode.DECRYPT != materials.cipherMode());
3436
}
3537

3638
CipherSubscriber(Subscriber<? super ByteBuffer> wrappedSubscriber, Long contentLength, CryptographicMaterials materials, byte[] iv) {
@@ -56,7 +58,9 @@ public void onNext(ByteBuffer byteBuffer) {
5658
// Note that while the JCE Javadoc specifies that the outputBuffer is null in this case,
5759
// in practice SunJCE and ACCP return an empty buffer instead, hence checks for
5860
// null OR length == 0.
59-
if (contentRead.get() + tagLength >= contentLength) {
61+
62+
// tagLength should only be added on Encrypt
63+
if (contentRead.get() + (isEncrypt ? tagLength : 0) >= contentLength) {
6064
// All content has been read, so complete to get the final bytes
6165
finalBytes();
6266
return;
@@ -84,7 +88,7 @@ public void onNext(ByteBuffer byteBuffer) {
8488
Calling `wrappedSubscriber.onNext` more than once for `request(1)`
8589
violates the Reactive Streams specification and can cause exceptions downstream.
8690
*/
87-
if (contentRead.get() + tagLength >= contentLength) {
91+
if (contentRead.get() + (isEncrypt ? tagLength : 0) >= contentLength) {
8892
// All content has been read; complete the stream.
8993
finalBytes();
9094
} else {
@@ -125,9 +129,10 @@ public void onError(Throwable t) {
125129
public void onComplete() {
126130
// In rare cases, e.g. when the last part of a low-level MPU has 0 length,
127131
// onComplete will be called before onNext is called once.
128-
if (contentRead.get() + tagLength <= contentLength) {
129-
finalBytes();
130-
}
132+
// So, call finalBytes here just in case there's any unsent data left.
133+
// Most likely, finalBytes has already been called by the last onNext,
134+
// but finalBytes guards against multiple invocations so it's safe to call again.
135+
finalBytes();
131136
wrappedSubscriber.onComplete();
132137
}
133138

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

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package software.amazon.encryption.s3.internal;
22

3+
import java.time.Instant;
4+
import java.util.Map;
5+
6+
import org.apache.commons.logging.LogFactory;
37
import software.amazon.awssdk.services.s3.model.ChecksumType;
8+
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse;
49
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
510
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
6-
import java.time.Instant;
7-
import java.util.Map;
11+
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
812

913
public class ConvertSDKRequests {
1014

11-
public static CreateMultipartUploadRequest convert(PutObjectRequest request) {
15+
public static CreateMultipartUploadRequest convertRequest(PutObjectRequest request) {
1216

1317
final CreateMultipartUploadRequest.Builder output = CreateMultipartUploadRequest.builder();
1418
request
@@ -121,7 +125,16 @@ public static CreateMultipartUploadRequest convert(PutObjectRequest request) {
121125
default:
122126
// Rather than silently dropping the value,
123127
// we loudly signal that we don't know how to handle this field.
124-
throw new IllegalArgumentException("Unknown PutObjectRequest field " + f.locationName() + ".");
128+
throw new IllegalArgumentException(
129+
f.locationName() + " is an unknown field. " +
130+
"The S3 Encryption Client does not recognize this option and cannot set it on the CreateMultipartUploadRequest." +
131+
"This may be a new S3 feature." +
132+
"Please report this to the Amazon S3 Encryption Client for Java: " +
133+
"https://github.com/aws/amazon-s3-encryption-client-java/issues." +
134+
"To work around this issue you can disable multi part upload," +
135+
"use the Async client, or not set this value on PutObject." +
136+
"You may be able to update this value after the PutObject request completes."
137+
);
125138
}
126139
}
127140
});
@@ -131,6 +144,77 @@ public static CreateMultipartUploadRequest convert(PutObjectRequest request) {
131144
.build();
132145
}
133146

147+
public static PutObjectResponse convertResponse(CompleteMultipartUploadResponse response) {
148+
final PutObjectResponse.Builder output = PutObjectResponse.builder();
149+
response
150+
.toBuilder()
151+
.sdkFields()
152+
.forEach(f -> {
153+
final Object value = f.getValueOrDefault(response);
154+
if (value != null) {
155+
switch (f.memberName()) {
156+
case "ETag":
157+
output.eTag((String) value);
158+
break;
159+
case "Expiration":
160+
output.expiration((String) value);
161+
break;
162+
case "ChecksumCRC32":
163+
output.checksumCRC32((String) value);
164+
break;
165+
case "ChecksumCRC32C":
166+
output.checksumCRC32C((String) value);
167+
break;
168+
case "ChecksumCRC64NVME":
169+
output.checksumCRC64NVME((String) value);
170+
break;
171+
case "ChecksumSHA1":
172+
output.checksumSHA1((String) value);
173+
break;
174+
case "ChecksumSHA256":
175+
output.checksumSHA256((String) value);
176+
break;
177+
case "ChecksumType":
178+
output.checksumType((String) value);
179+
break;
180+
case "ServerSideEncryption":
181+
output.serverSideEncryption((String) value);
182+
break;
183+
case "VersionId":
184+
output.versionId((String) value);
185+
break;
186+
case "SSEKMSKeyId":
187+
output.ssekmsKeyId((String) value);
188+
break;
189+
case "BucketKeyEnabled":
190+
output.bucketKeyEnabled((Boolean) value);
191+
break;
192+
case "RequestCharged":
193+
output.requestCharged((String) value);
194+
break;
195+
// Ignored fields: Location, Bucket, Key
196+
case "Location":
197+
case "Bucket":
198+
case "Key":
199+
// These fields exist only in CompleteMultipartUploadResponse, not in PutObjectResponse
200+
break;
201+
default:
202+
// We should NOT throw an exception for unknown fields because
203+
// once the object is stored, we expect to return a successful response.
204+
// Emit a log at info level for awareness.
205+
LogFactory.getLog(ConvertSDKRequests.class).info(f.memberName() + " returned in CompleteMultipartUploadResponse for "
206+
+ response.key() + " is an unknown field." +
207+
"The S3 Encryption Client does not recognize this option and cannot set it on the CompleteMultipartUploadResponse." +
208+
"This may be a new S3 feature." +
209+
"Please report this to the Amazon S3 Encryption Client for Java: " +
210+
"https://github.com/aws/amazon-s3-encryption-client-java/issues."
211+
);
212+
}
213+
}
214+
});
215+
return output.build();
216+
}
217+
134218
private static boolean isStringStringMap(Object value) {
135219
if (!(value instanceof Map)) {
136220
return false;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public UploadObjectObserver init(PutObjectRequest req,
4444

4545
public String onUploadCreation(PutObjectRequest req) {
4646
CreateMultipartUploadResponse res =
47-
s3EncryptionClient.createMultipartUpload(ConvertSDKRequests.convert(req));
47+
s3EncryptionClient.createMultipartUpload(ConvertSDKRequests.convertRequest(req));
4848
return this.uploadId = res.uploadId();
4949
}
5050

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,16 +910,46 @@ public void wrappedClientMultipartUploadThrowsException() throws IOException {
910910
}
911911

912912
@Test
913-
public void S3AsyncClientBuilderForbidsMultipartEnabled() throws IOException {
913+
public void s3AsyncClientBuilderForbidsMultipartEnabled() {
914914
assertThrows(
915915
UnsupportedOperationException.class,
916916
() -> S3AsyncEncryptionClient.builder().multipartEnabled(Boolean.TRUE));
917917
}
918918

919919
@Test
920-
public void S3AsyncClientBuilderForbidsMultipartConfiguration() throws IOException {
920+
public void s3AsyncClientBuilderForbidsMultipartConfiguration() {
921921
assertThrows(
922922
UnsupportedOperationException.class,
923923
() -> S3AsyncEncryptionClient.builder().multipartConfiguration(MultipartConfiguration.builder().build()));
924924
}
925+
926+
@Test
927+
public void s3AsyncClientForbidsCreateMultipartUpload() {
928+
S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builder()
929+
.kmsKeyId("fails")
930+
.build();
931+
932+
assertThrows(UnsupportedOperationException.class, () -> s3AsyncClient.createMultipartUpload(builder -> builder.bucket(BUCKET).key("expected-fail").build()).join());
933+
s3AsyncClient.close();
934+
}
935+
936+
@Test
937+
public void s3AsyncClientForbidsUploadPart() {
938+
S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builder()
939+
.kmsKeyId("fails")
940+
.build();
941+
942+
assertThrows(UnsupportedOperationException.class, () -> s3AsyncClient.uploadPart(builder -> builder.uploadId("fail").build(), AsyncRequestBody.fromString("fail")).join());
943+
s3AsyncClient.close();
944+
}
945+
946+
@Test
947+
public void s3AsyncClientForbidsCompleteMultipartUpload() {
948+
S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builder()
949+
.kmsKeyId("fails")
950+
.build();
951+
952+
assertThrows(UnsupportedOperationException.class, () -> s3AsyncClient.completeMultipartUpload(builder -> builder.uploadId("fail").build()).join());
953+
s3AsyncClient.close();
954+
}
925955
}

0 commit comments

Comments
 (0)