public UploadPartResponse uploadPart()

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;
    }