in src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java [88:172]
public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody)
throws AwsServiceException, SdkClientException {
final AlgorithmSuite algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
final int blockSize = algorithmSuite.cipherBlockSizeBytes();
// Validate the partSize / contentLength in the request and requestBody
// There is similar logic in PutEncryptedObjectPipeline,
// but this uses non-async requestBody, so the code is not shared
final long partContentLength;
if (request.contentLength() != null) {
if (requestBody.optionalContentLength().isPresent() && !request.contentLength().equals(requestBody.optionalContentLength().get())) {
// if the contentLength values do not match, throw an exception, since we don't know which is correct
throw new S3EncryptionClientException("The contentLength provided in the request object MUST match the " +
"contentLength in the request body");
} else if (!requestBody.optionalContentLength().isPresent()) {
// no contentLength in request body, use the one in request
partContentLength = request.contentLength();
} else {
// only remaining case is when the values match, so either works here
partContentLength = request.contentLength();
}
} else {
partContentLength = requestBody.optionalContentLength().orElse(-1L);
}
final boolean isLastPart = request.sdkPartType() != null && request.sdkPartType().equals(SdkPartType.LAST);
final int cipherTagLength = isLastPart ? algorithmSuite.cipherTagLengthBytes() : 0;
final long ciphertextLength = partContentLength + cipherTagLength;
final boolean partSizeMultipleOfCipherBlockSize = 0 == (partContentLength % blockSize);
if (!isLastPart && !partSizeMultipleOfCipherBlockSize) {
throw new S3EncryptionClientException("Invalid part size: part sizes for encrypted multipart uploads must " +
"be multiples of the cipher block size (" + blockSize + ") with the exception of the last part.");
}
// Once we have (a valid) ciphertext length, set the request contentLength
UploadPartRequest actualRequest = request.toBuilder()
.overrideConfiguration(API_NAME_INTERCEPTOR)
.contentLength(ciphertextLength)
.build();
final String uploadId = actualRequest.uploadId();
final MultipartUploadMaterials materials = _multipartUploadMaterials.get(uploadId);
if (materials == null) {
throw new S3EncryptionClientException("No client-side information available on upload ID " + uploadId);
}
final UploadPartResponse response;
// Checks the parts are uploaded in series
materials.beginPartUpload(actualRequest.partNumber(), partContentLength);
Cipher cipher = materials.getCipher(materials.getIv());
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
try {
final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody(
AsyncRequestBody.fromInputStream(
requestBody.contentStreamProvider().newStream(),
partContentLength, // this MUST be the original contentLength; it refers to the plaintext stream
singleThreadExecutor
),
ciphertextLength, materials, cipher.getIV(), isLastPart
);
// Ensure we haven't already seen the last part
if (isLastPart) {
if (materials.hasFinalPartBeenSeen()) {
throw new S3EncryptionClientException("This part was specified as the last part in a multipart " +
"upload, but a previous part was already marked as the last part. Only the last part of the " +
"upload should be marked as the last part.");
}
}
// Ensures parts are not retried to avoid corrupting ciphertext
AsyncRequestBody noRetryBody = new NoRetriesAsyncRequestBody(cipherAsyncRequestBody);
response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join();
} finally {
materials.endPartUpload();
}
if (isLastPart) {
materials.setHasFinalPartBeenSeen(true);
}
singleThreadExecutor.shutdown();
return response;
}