@Override
  public CopyPartResult copyPart(CopyPartRequest copyPartRequest) {
    String uploadId = copyPartRequest.getUploadId();
    EncryptedUploadContext encryptedUploadContext = currentMultipartUploadSecretKeys.get(uploadId);

    if (!encryptedUploadContext.hasFinalPartBeenSeen()) {
      encryptedUploadContext.setHasFinalPartBeenSeen(true);
    }

    return super.copyPart(copyPartRequest);
  }
  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3Client#initiateMultipartUpload(com.amazonaws.services.s3.model.InitiateMultipartUploadRequest)
   */
  @Override
  public InitiateMultipartUploadResult initiateMultipartUpload(
      InitiateMultipartUploadRequest initiateMultipartUploadRequest)
      throws AmazonClientException, AmazonServiceException {

    appendUserAgent(initiateMultipartUploadRequest, USER_AGENT);

    // Generate a one-time use symmetric key and initialize a cipher to encrypt object data
    SecretKey envelopeSymmetricKey = EncryptionUtils.generateOneTimeUseSymmetricKey();
    Cipher symmetricCipher =
        EncryptionUtils.createSymmetricCipher(
            envelopeSymmetricKey, Cipher.ENCRYPT_MODE, cryptoConfig.getCryptoProvider(), null);

    if (cryptoConfig.getStorageMode() == CryptoStorageMode.ObjectMetadata) {
      EncryptionMaterials encryptionMaterials =
          encryptionMaterialsProvider.getEncryptionMaterials();
      // Encrypt the envelope symmetric key
      byte[] encryptedEnvelopeSymmetricKey =
          EncryptionUtils.getEncryptedSymmetricKey(
              envelopeSymmetricKey, encryptionMaterials, cryptoConfig.getCryptoProvider());

      // Store encryption info in metadata
      ObjectMetadata metadata =
          EncryptionUtils.updateMetadataWithEncryptionInfo(
              initiateMultipartUploadRequest,
              encryptedEnvelopeSymmetricKey,
              symmetricCipher,
              encryptionMaterials.getMaterialsDescription());

      // Update the request's metadata to the updated metadata
      initiateMultipartUploadRequest.setObjectMetadata(metadata);
    }

    InitiateMultipartUploadResult result =
        super.initiateMultipartUpload(initiateMultipartUploadRequest);
    EncryptedUploadContext encryptedUploadContext =
        new EncryptedUploadContext(
            initiateMultipartUploadRequest.getBucketName(),
            initiateMultipartUploadRequest.getKey(),
            envelopeSymmetricKey);
    encryptedUploadContext.setNextInitializationVector(symmetricCipher.getIV());
    encryptedUploadContext.setFirstInitializationVector(symmetricCipher.getIV());
    currentMultipartUploadSecretKeys.put(result.getUploadId(), encryptedUploadContext);

    return result;
  }
  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3Client#completeMultipartUpload(com.amazonaws.services.s3.model.CompleteMultipartUploadRequest)
   */
  @Override
  public CompleteMultipartUploadResult completeMultipartUpload(
      CompleteMultipartUploadRequest completeMultipartUploadRequest)
      throws AmazonClientException, AmazonServiceException {

    appendUserAgent(completeMultipartUploadRequest, USER_AGENT);

    String uploadId = completeMultipartUploadRequest.getUploadId();
    EncryptedUploadContext encryptedUploadContext = currentMultipartUploadSecretKeys.get(uploadId);

    if (encryptedUploadContext.hasFinalPartBeenSeen() == false) {
      throw new AmazonClientException(
          "Unable to complete an encrypted multipart upload without being told which part was the last.  "
              + "Without knowing which part was the last, the encrypted data in Amazon S3 is incomplete and corrupt.");
    }

    CompleteMultipartUploadResult result =
        super.completeMultipartUpload(completeMultipartUploadRequest);

    // In InstructionFile mode, we want to write the instruction file only after the whole upload
    // has completed correctly.
    if (cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) {
      Cipher symmetricCipher =
          EncryptionUtils.createSymmetricCipher(
              encryptedUploadContext.getEnvelopeEncryptionKey(),
              Cipher.ENCRYPT_MODE,
              cryptoConfig.getCryptoProvider(),
              encryptedUploadContext.getFirstInitializationVector());

      EncryptionMaterials encryptionMaterials =
          encryptionMaterialsProvider.getEncryptionMaterials();

      // Encrypt the envelope symmetric key
      byte[] encryptedEnvelopeSymmetricKey =
          EncryptionUtils.getEncryptedSymmetricKey(
              encryptedUploadContext.getEnvelopeEncryptionKey(),
              encryptionMaterials,
              cryptoConfig.getCryptoProvider());
      EncryptionInstruction instruction =
          new EncryptionInstruction(
              encryptionMaterials.getMaterialsDescription(),
              encryptedEnvelopeSymmetricKey,
              encryptedUploadContext.getEnvelopeEncryptionKey(),
              symmetricCipher);

      // Put the instruction file into S3
      super.putObject(
          EncryptionUtils.createInstructionPutRequest(
              encryptedUploadContext.getBucketName(),
              encryptedUploadContext.getKey(),
              instruction));
    }

    currentMultipartUploadSecretKeys.remove(uploadId);
    return result;
  }
  /**
   * {@inheritDoc}
   *
   * <p><b>NOTE:</b> Because the encryption process requires context from block N-1 in order to
   * encrypt block N, parts uploaded with the AmazonS3EncryptionClient (as opposed to the normal
   * AmazonS3Client) must be uploaded serially, and in order. Otherwise, the previous encryption
   * context isn't available to use when encrypting the current part.
   */
  @Override
  public UploadPartResult uploadPart(UploadPartRequest uploadPartRequest)
      throws AmazonClientException, AmazonServiceException {

    appendUserAgent(uploadPartRequest, USER_AGENT);

    boolean isLastPart = uploadPartRequest.isLastPart();
    String uploadId = uploadPartRequest.getUploadId();

    boolean partSizeMultipleOfCipherBlockSize =
        uploadPartRequest.getPartSize() % JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE == 0;
    if (!isLastPart && !partSizeMultipleOfCipherBlockSize) {
      throw new AmazonClientException(
          "Invalid part size: part sizes for encrypted multipart uploads must be multiples "
              + "of the cipher block size ("
              + JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE
              + ") with the exception of the last part.  "
              + "Otherwise encryption adds extra padding that will corrupt the final object.");
    }

    // Generate the envelope symmetric key and initialize a cipher to encrypt the object's data
    EncryptedUploadContext encryptedUploadContext = currentMultipartUploadSecretKeys.get(uploadId);
    if (encryptedUploadContext == null)
      throw new AmazonClientException(
          "No client-side information available on upload ID " + uploadId);

    SecretKey envelopeSymmetricKey = encryptedUploadContext.getEnvelopeEncryptionKey();
    byte[] iv = encryptedUploadContext.getNextInitializationVector();
    CipherFactory cipherFactory =
        new CipherFactory(
            envelopeSymmetricKey, Cipher.ENCRYPT_MODE, iv, this.cryptoConfig.getCryptoProvider());

    // Create encrypted input stream
    InputStream encryptedInputStream =
        EncryptionUtils.getEncryptedInputStream(uploadPartRequest, cipherFactory);
    uploadPartRequest.setInputStream(encryptedInputStream);

    // The last part of the multipart upload will contain extra padding from the encryption process,
    // which
    // changes the
    if (uploadPartRequest.isLastPart()) {
      // We only change the size of the last part
      long cryptoContentLength =
          EncryptionUtils.calculateCryptoContentLength(
              cipherFactory.createCipher(), uploadPartRequest);
      if (cryptoContentLength > 0) uploadPartRequest.setPartSize(cryptoContentLength);

      if (encryptedUploadContext.hasFinalPartBeenSeen()) {
        throw new AmazonClientException(
            "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, otherwise it will cause the encrypted data to be corrupted.");
      }

      encryptedUploadContext.setHasFinalPartBeenSeen(true);
    }

    // Treat all encryption requests as input stream upload requests, not as file upload requests.
    uploadPartRequest.setFile(null);
    uploadPartRequest.setFileOffset(0);

    UploadPartResult result = super.uploadPart(uploadPartRequest);

    if (encryptedInputStream instanceof ByteRangeCapturingInputStream) {
      ByteRangeCapturingInputStream bris = (ByteRangeCapturingInputStream) encryptedInputStream;
      encryptedUploadContext.setNextInitializationVector(bris.getBlock());
    } else {
      throw new AmazonClientException("Unable to access last block of encrypted data");
    }

    return result;
  }