Example #1
0
  /** Fetches a file from the URL given and returns an input stream to it. */
  private static InputStream fetchFile(String url)
      throws IOException, ClientProtocolException, FileNotFoundException {

    HttpParams httpClientParams = new BasicHttpParams();
    HttpProtocolParams.setUserAgent(httpClientParams, VersionInfoUtils.getUserAgent());
    HttpClient httpclient = new DefaultHttpClient(httpClientParams);
    HttpGet httpget = new HttpGet(url);
    HttpResponse response = httpclient.execute(httpget);
    HttpEntity entity = response.getEntity();
    if (entity != null) {
      return entity.getContent();
    }
    return null;
  }
/**
 * Client configuration options such as proxy settings, user agent string, max retry attempts, etc.
 */
@NotThreadSafe
public class ClientConfiguration {

  /** The default timeout for a connected socket. */
  public static final int DEFAULT_SOCKET_TIMEOUT = 50 * 1000;

  /** The default max connection pool size. */
  public static final int DEFAULT_MAX_CONNECTIONS = 50;

  /** The default HTTP user agent header for AWS Java SDK clients. */
  public static final String DEFAULT_USER_AGENT = VersionInfoUtils.getUserAgent();

  /** The default maximum number of retries for error responses. */
  public static final int DEFAULT_MAX_RETRIES = 3;

  /**
   * The default on whether to use the {@link IdleConnectionReaper} to manage stale connections
   *
   * @see IdleConnectionReaper
   */
  public static final boolean DEFAULT_USE_REAPER = true;

  /** The HTTP user agent header passed with all HTTP requests. */
  private String userAgent = DEFAULT_USER_AGENT;

  /**
   * The maximum number of times that a retryable failed request (ex: a 5xx response from a service)
   * will be retried.
   */
  private int maxErrorRetry = DEFAULT_MAX_RETRIES;

  /**
   * The protocol to use when connecting to Amazon Web Services.
   *
   * <p>The default configuration is to use HTTPS for all requests for increased security.
   */
  private Protocol protocol = Protocol.HTTPS;

  /** Optionally specifies the proxy host to connect through. */
  private String proxyHost = null;

  /** Optionally specifies the port on the proxy host to connect through. */
  private int proxyPort = -1;

  /** Optionally specifies the user name to use when connecting through a proxy. */
  private String proxyUsername = null;

  /** Optionally specifies the password to use when connecting through a proxy. */
  private String proxyPassword = null;

  /** Optional Windows domain name for configuring NTLM proxy support. */
  private String proxyDomain = null;

  /** Optional Windows workstation name for configuring NTLM proxy support. */
  private String proxyWorkstation = null;

  /** The maximum number of open HTTP connections. */
  private int maxConnections = DEFAULT_MAX_CONNECTIONS;

  /**
   * The amount of time to wait (in milliseconds) for data to be transfered over an established,
   * open connection before the connection is timed out. A value of 0 means infinity, and is not
   * recommended.
   */
  private int socketTimeout = DEFAULT_SOCKET_TIMEOUT;

  /**
   * The amount of time to wait (in milliseconds) when initially establishing a connection before
   * giving up and timing out. A value of 0 means infinity, and is not recommended.
   */
  private int connectionTimeout = 50 * 1000;

  /**
   * Optional size hint (in bytes) for the low level TCP send buffer. This is an advanced option for
   * advanced users who want to tune low level TCP parameters to try and squeeze out more
   * performance.
   */
  private int socketSendBufferSizeHint = 0;

  /**
   * Optional size hint (in bytes) for the low level TCP receive buffer. This is an advanced option
   * for advanced users who want to tune low level TCP parameters to try and squeeze out more
   * performance.
   */
  private int socketReceiveBufferSizeHint = 0;

  /**
   * Optional whether to use the {@link IdleConnectionReaper} to manage stale connections. A reason
   * for not running the {@link IdleConnectionReaper} can be if running in an environment where the
   * modifyThread and modifyThreadGroup permissions are not allowed.
   */
  private boolean useReaper = DEFAULT_USE_REAPER;

  public ClientConfiguration() {}

  public ClientConfiguration(ClientConfiguration other) {
    this.connectionTimeout = other.connectionTimeout;
    this.maxConnections = other.maxConnections;
    this.maxErrorRetry = other.maxErrorRetry;
    this.protocol = other.protocol;
    this.proxyDomain = other.proxyDomain;
    this.proxyHost = other.proxyHost;
    this.proxyPassword = other.proxyPassword;
    this.proxyPort = other.proxyPort;
    this.proxyUsername = other.proxyUsername;
    this.proxyWorkstation = other.proxyWorkstation;
    this.socketTimeout = other.socketTimeout;
    this.userAgent = other.userAgent;
    this.useReaper = other.useReaper;

    this.socketReceiveBufferSizeHint = other.socketReceiveBufferSizeHint;
    this.socketSendBufferSizeHint = other.socketSendBufferSizeHint;
  }

  /**
   * Returns the protocol (HTTP or HTTPS) to use when connecting to Amazon Web Services.
   *
   * <p>The default configuration is to use HTTPS for all requests for increased security.
   *
   * <p>Individual clients can also override this setting by explicitly including the protocol as
   * part of the endpoint URL when calling {@link AmazonWebServiceClient#setEndpoint(String)}.
   *
   * @return The protocol to use when connecting to Amazon Web Services.
   */
  public Protocol getProtocol() {
    return protocol;
  }

  /**
   * Sets the protocol (i.e. HTTP or HTTPS) to use when connecting to Amazon Web Services.
   *
   * <p>The default configuration is to use HTTPS for all requests for increased security.
   *
   * <p>Individual clients can also override this setting by explicitly including the protocol as
   * part of the endpoint URL when calling {@link AmazonWebServiceClient#setEndpoint(String)}.
   *
   * @param protocol The protocol to use when connecting to Amazon Web Services.
   */
  public void setProtocol(Protocol protocol) {
    this.protocol = protocol;
  }

  /**
   * Sets the protocol (i.e. HTTP or HTTPS) to use when connecting to Amazon Web Services, and
   * returns the updated ClientConfiguration object so that additional calls may be chained
   * together.
   *
   * <p>The default configuration is to use HTTPS for all requests for increased security.
   *
   * <p>Individual clients can also override this setting by explicitly including the protocol as
   * part of the endpoint URL when calling {@link AmazonWebServiceClient#setEndpoint(String)}.
   *
   * @param protocol The protocol to use when connecting to Amazon Web Services.
   * @return The updated ClientConfiguration object with the new max HTTP connections setting.
   */
  public ClientConfiguration withProtocol(Protocol protocol) {
    setProtocol(protocol);
    return this;
  }

  /**
   * Returns the maximum number of allowed open HTTP connections.
   *
   * @return The maximum number of allowed open HTTP connections.
   */
  public int getMaxConnections() {
    return maxConnections;
  }

  /**
   * Sets the maximum number of allowed open HTTP connections.
   *
   * @param maxConnections The maximum number of allowed open HTTP connections.
   */
  public void setMaxConnections(int maxConnections) {
    this.maxConnections = maxConnections;
  }

  /**
   * Sets the maximum number of allowed open HTTP connections and returns the updated
   * ClientConfiguration object.
   *
   * @param maxConnections The maximum number of allowed open HTTP connections.
   * @return The updated ClientConfiguration object with the new max HTTP connections setting.
   */
  public ClientConfiguration withMaxConnections(int maxConnections) {
    setMaxConnections(maxConnections);
    return this;
  }

  /**
   * Returns the HTTP user agent header to send with all requests.
   *
   * @return The user agent string to use when sending requests.
   */
  public String getUserAgent() {
    return userAgent;
  }

  /**
   * Sets the HTTP user agent header to send with all requests.
   *
   * @param userAgent The user agent string to use when sending requests.
   */
  public void setUserAgent(String userAgent) {
    this.userAgent = userAgent;
  }

  /**
   * Sets the HTTP user agent header used in requests and returns the updated ClientConfiguration
   * object.
   *
   * @param userAgent The user agent string to use when sending requests.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withUserAgent(String userAgent) {
    setUserAgent(userAgent);
    return this;
  }

  /**
   * Returns the optional proxy host the client will connect through.
   *
   * @return The proxy host the client will connect through.
   */
  public String getProxyHost() {
    return proxyHost;
  }

  /**
   * Sets the optional proxy host the client will connect through.
   *
   * @param proxyHost The proxy host the client will connect through.
   */
  public void setProxyHost(String proxyHost) {
    this.proxyHost = proxyHost;
  }

  /**
   * Sets the optional proxy host the client will connect through and returns the updated
   * ClientConfiguration object.
   *
   * @param proxyHost The proxy host the client will connect through.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyHost(String proxyHost) {
    setProxyHost(proxyHost);
    return this;
  }

  /**
   * Returns the optional proxy port the client will connect through.
   *
   * @return The proxy port the client will connect through.
   */
  public int getProxyPort() {
    return proxyPort;
  }

  /**
   * Sets the optional proxy port the client will connect through.
   *
   * @param proxyPort The proxy port the client will connect through.
   */
  public void setProxyPort(int proxyPort) {
    this.proxyPort = proxyPort;
  }

  /**
   * Sets the optional proxy port the client will connect through and returns the updated
   * ClientConfiguration object.
   *
   * @param proxyPort The proxy port the client will connect through.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyPort(int proxyPort) {
    setProxyPort(proxyPort);
    return this;
  }

  /**
   * Returns the optional proxy user name to use if connecting through a proxy.
   *
   * @return The optional proxy user name the configured client will use if connecting through a
   *     proxy.
   */
  public String getProxyUsername() {
    return proxyUsername;
  }

  /**
   * Sets the optional proxy user name to use if connecting through a proxy.
   *
   * @param proxyUsername The proxy user name to use if connecting through a proxy.
   */
  public void setProxyUsername(String proxyUsername) {
    this.proxyUsername = proxyUsername;
  }

  /**
   * Sets the optional proxy user name and returns the updated ClientConfiguration object.
   *
   * @param proxyUsername The proxy user name to use if connecting through a proxy.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyUsername(String proxyUsername) {
    setProxyUsername(proxyUsername);
    return this;
  }

  /**
   * Returns the optional proxy password to use when connecting through a proxy.
   *
   * @return The password to use when connecting through a proxy.
   */
  public String getProxyPassword() {
    return proxyPassword;
  }

  /**
   * Sets the optional proxy password to use when connecting through a proxy.
   *
   * @param proxyPassword The password to use when connecting through a proxy.
   */
  public void setProxyPassword(String proxyPassword) {
    this.proxyPassword = proxyPassword;
  }

  /**
   * Sets the optional proxy password to use when connecting through a proxy, and returns the
   * updated ClientConfiguration object.
   *
   * @param proxyPassword The password to use when connecting through a proxy.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyPassword(String proxyPassword) {
    setProxyPassword(proxyPassword);
    return this;
  }

  /**
   * Returns the optional Windows domain name for configuring an NTLM proxy. If you aren't using a
   * Windows NTLM proxy, you do not need to set this field.
   *
   * @return The optional Windows domain name for configuring an NTLM proxy.
   */
  public String getProxyDomain() {
    return proxyDomain;
  }

  /**
   * Sets the optional Windows domain name for configuration an NTLM proxy. If you aren't using a
   * Windows NTLM proxy, you do not need to set this field.
   *
   * @param proxyDomain The optional Windows domain name for configuring an NTLM proxy.
   */
  public void setProxyDomain(String proxyDomain) {
    this.proxyDomain = proxyDomain;
  }

  /**
   * Sets the optional Windows domain name for configuration an NTLM proxy and returns a reference
   * to this updated ClientConfiguration object so that additional method calls can be chained
   * together. If you aren't using a Windows NTLM proxy, you do not need to set this field.
   *
   * @param proxyDomain The optional Windows domain name for configuring an NTLM proxy.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyDomain(String proxyDomain) {
    setProxyDomain(proxyDomain);
    return this;
  }

  /**
   * Returns the optional Windows workstation name for configuring NTLM proxy support. If you aren't
   * using a Windows NTLM proxy, you do not need to set this field.
   *
   * @return The optional Windows workstation name for configuring NTLM proxy support.
   */
  public String getProxyWorkstation() {
    return proxyWorkstation;
  }

  /**
   * Sets the optional Windows workstation name for configuring NTLM proxy support. If you aren't
   * using a Windows NTLM proxy, you do not need to set this field.
   *
   * @param proxyWorkstation The optional Windows workstation name for configuring NTLM proxy
   *     support.
   */
  public void setProxyWorkstation(String proxyWorkstation) {
    this.proxyWorkstation = proxyWorkstation;
  }

  /**
   * Sets the optional Windows workstation name for configuring NTLM proxy support, and returns the
   * updated ClientConfiguration object so that additional method calls can be chained together. If
   * you aren't using a Windows NTLM proxy, you do not need to set this field.
   *
   * @param proxyWorkstation The optional Windows workstation name for configuring NTLM proxy
   *     support.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withProxyWorkstation(String proxyWorkstation) {
    setProxyWorkstation(proxyWorkstation);
    return this;
  }

  /**
   * Returns the maximum number of retry attempts for failed retryable requests (ex: 5xx error
   * responses from a service).
   *
   * @return The maximum number of retry attempts for failed retryable requests.
   */
  public int getMaxErrorRetry() {
    return maxErrorRetry;
  }

  /**
   * Sets the maximum number of retry attempts for failed retryable requests (ex: 5xx error
   * responses from services).
   *
   * @param maxErrorRetry The maximum number of retry attempts for failed retryable requests.
   */
  public void setMaxErrorRetry(int maxErrorRetry) {
    this.maxErrorRetry = maxErrorRetry;
  }

  /**
   * Sets the maximum number of retry attempts for failed retryable requests (ex: 5xx error
   * responses from services), and returns the updated ClientConfiguration object.
   *
   * @param maxErrorRetry The maximum number of retry attempts for failed retryable requests.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withMaxErrorRetry(int maxErrorRetry) {
    setMaxErrorRetry(maxErrorRetry);
    return this;
  }

  /**
   * Returns the amount of time to wait (in milliseconds) for data to be transfered over an
   * established, open connection before the connection times out and is closed. A value of 0 means
   * infinity, and isn't recommended.
   *
   * @return The amount of time to wait (in milliseconds) for data to be transfered over an
   *     established, open connection before the connection times out and is closed.
   */
  public int getSocketTimeout() {
    return socketTimeout;
  }

  /**
   * Sets the amount of time to wait (in milliseconds) for data to be transfered over an
   * established, open connection before the connection times out and is closed. A value of 0 means
   * infinity, and isn't recommended.
   *
   * @param socketTimeout The amount of time to wait (in milliseconds) for data to be transfered
   *     over an established, open connection before the connection is times out and is closed.
   */
  public void setSocketTimeout(int socketTimeout) {
    this.socketTimeout = socketTimeout;
  }

  /**
   * Sets the amount of time to wait (in milliseconds) for data to be transfered over an
   * established, open connection before the connection times out and is closed, and returns the
   * updated ClientConfiguration object so that additional method calls may be chained together.
   *
   * @param socketTimeout The amount of time to wait (in milliseconds) for data to be transfered
   *     over an established, open connection before the connection is times out and is closed.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withSocketTimeout(int socketTimeout) {
    setSocketTimeout(socketTimeout);
    return this;
  }

  /**
   * Returns the amount of time to wait (in milliseconds) when initially establishing a connection
   * before giving up and timing out. A value of 0 means infinity, and is not recommended.
   *
   * @return The amount of time to wait (in milliseconds) when initially establishing a connection
   *     before giving up and timing out.
   */
  public int getConnectionTimeout() {
    return connectionTimeout;
  }

  /**
   * Sets the amount of time to wait (in milliseconds) when initially establishing a connection
   * before giving up and timing out. A value of 0 means infinity, and is not recommended.
   *
   * @param connectionTimeout The amount of time to wait (in milliseconds) when initially
   *     establishing a connection before giving up and timing out.
   */
  public void setConnectionTimeout(int connectionTimeout) {
    this.connectionTimeout = connectionTimeout;
  }

  /**
   * Sets the amount of time to wait (in milliseconds) when initially establishing a connection
   * before giving up and timing out, and returns the updated ClientConfiguration object so that
   * additional method calls may be chained together.
   *
   * @param connectionTimeout the amount of time to wait (in milliseconds) when initially
   *     establishing a connection before giving up and timing out.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withConnectionTimeout(int connectionTimeout) {
    setConnectionTimeout(connectionTimeout);
    return this;
  }

  /**
   * Checks if the {@link IdleConnectionReaper} is to be started
   *
   * @return if the {@link IdleConnectionReaper} is to be started
   */
  public boolean useReaper() {
    return useReaper;
  }

  /**
   * Sets whether the {@link IdleConnectionReaper} is to be started as a daemon thread
   *
   * @param use whether the {@link IdleConnectionReaper} is to be started as a daemon thread
   * @see IdleConnectionReaper
   */
  public void setUseReaper(boolean use) {
    this.useReaper = use;
  }

  /**
   * Sets whether the {@link IdleConnectionReaper} is to be started as a daemon thread
   *
   * @param use the {@link IdleConnectionReaper} is to be started as a daemon thread
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withReaper(boolean use) {
    setUseReaper(use);
    return this;
  }

  /**
   * Returns the optional size hints (in bytes) for the low level TCP send and receive buffers. This
   * is an advanced option for advanced users who want to tune low level TCP parameters to try and
   * squeeze out more performance.
   *
   * <p>The optimal TCP buffer sizes for a particular application are highly dependent on network
   * configuration and operating system configuration and capabilities. For example, most modern
   * operating systems provide auto-tuning functionality for TCP buffer sizes, which can have a big
   * impact on performance for TCP connections that are held open long enough for the auto-tuning to
   * optimize buffer sizes.
   *
   * <p>Large buffer sizes (ex: 2MB) will allow the operating system to buffer more data in memory
   * without requiring the remote server to acknowledge receipt of that information, so can be
   * particularly useful when the network has high latency.
   *
   * <p>This is only a <b>hint</b>, and the operating system may choose not to honor it. When using
   * this option, users should <b>always</b> check the operating system's configured limits and
   * defaults. Most OS's have a maximum TCP buffer size limit configured, and won't let you go
   * beyond that limit unless you explicitly raise the max TCP buffer size limit.
   *
   * <p>There are many resources available online to help with configuring TCP buffer sizes and
   * operating system specific TCP settings, including:
   *
   * <ul>
   *   <li>http://onlamp.com/pub/a/onlamp/2005/11/17/tcp_tuning.html
   *   <li>http://fasterdata.es.net/TCP-tuning/
   * </ul>
   *
   * @return A two element array containing first the TCP send buffer size hint and then the TCP
   *     receive buffer size hint.
   */
  public int[] getSocketBufferSizeHints() {
    return new int[] {socketSendBufferSizeHint, socketReceiveBufferSizeHint};
  }

  /**
   * Sets the optional size hints (in bytes) for the low level TCP send and receive buffers. This is
   * an advanced option for advanced users who want to tune low level TCP parameters to try and
   * squeeze out more performance.
   *
   * <p>The optimal TCP buffer sizes for a particular application are highly dependent on network
   * configuration and operating system configuration and capabilities. For example, most modern
   * operating systems provide auto-tuning functionality for TCP buffer sizes, which can have a big
   * impact on performance for TCP connections that are held open long enough for the auto-tuning to
   * optimize buffer sizes.
   *
   * <p>Large buffer sizes (ex: 2MB) will allow the operating system to buffer more data in memory
   * without requiring the remote server to acknowledge receipt of that information, so can be
   * particularly useful when the network has high latency.
   *
   * <p>This is only a <b>hint</b>, and the operating system may choose not to honor it. When using
   * this option, users should <b>always</b> check the operating system's configured limits and
   * defaults. Most OS's have a maximum TCP buffer size limit configured, and won't let you go
   * beyond that limit unless you explicitly raise the max TCP buffer size limit.
   *
   * <p>There are many resources available online to help with configuring TCP buffer sizes and
   * operating system specific TCP settings, including:
   *
   * <ul>
   *   <li>http://onlamp.com/pub/a/onlamp/2005/11/17/tcp_tuning.html
   *   <li>http://fasterdata.es.net/TCP-tuning/
   * </ul>
   *
   * @param socketSendBufferSizeHint The size hint (in bytes) for the low level TCP send buffer.
   * @param socketReceiveBufferSizeHint The size hint (in bytes) for the low level TCP receive
   *     buffer.
   */
  public void setSocketBufferSizeHints(
      int socketSendBufferSizeHint, int socketReceiveBufferSizeHint) {
    this.socketSendBufferSizeHint = socketSendBufferSizeHint;
    this.socketReceiveBufferSizeHint = socketReceiveBufferSizeHint;
  }

  /**
   * Sets the optional size hints (in bytes) for the low level TCP send and receive buffers, and
   * returns the updated ClientConfiguration object so that additional method calls may be chained
   * together.
   *
   * <p>This is an advanced option for advanced users who want to tune low level TCP parameters to
   * try and squeeze out more performance.
   *
   * <p>The optimal TCP buffer sizes for a particular application are highly dependent on network
   * configuration and operating system configuration and capabilities. For example, most modern
   * operating systems provide auto-tuning functionality for TCP buffer sizes, which can have a big
   * impact on performance for TCP connections that are held open long enough for the auto-tuning to
   * optimize buffer sizes.
   *
   * <p>Large buffer sizes (ex: 2MB) will allow the operating system to buffer more data in memory
   * without requiring the remote server to acknowledge receipt of that information, so can be
   * particularly useful when the network has high latency.
   *
   * <p>This is only a <b>hint</b>, and the operating system may choose not to honor it. When using
   * this option, users should <b>always</b> check the operating system's configured limits and
   * defaults. Most OS's have a maximum TCP buffer size limit configured, and won't let you go
   * beyond that limit unless you explicitly raise the max TCP buffer size limit.
   *
   * <p>There are many resources available online to help with configuring TCP buffer sizes and
   * operating system specific TCP settings, including:
   *
   * <ul>
   *   <li>http://onlamp.com/pub/a/onlamp/2005/11/17/tcp_tuning.html
   *   <li>http://fasterdata.es.net/TCP-tuning/
   * </ul>
   *
   * @param socketSendBufferSizeHint The size hint (in bytes) for the low level TCP send buffer.
   * @param socketReceiveBufferSizeHint The size hint (in bytes) for the low level TCP receive
   *     buffer.
   * @return The updated ClientConfiguration object.
   */
  public ClientConfiguration withSocketBufferSizeHints(
      int socketSendBufferSizeHint, int socketReceiveBufferSizeHint) {
    setSocketBufferSizeHints(socketSendBufferSizeHint, socketReceiveBufferSizeHint);
    return this;
  }
}
/**
 * The AmazonS3Encryption class extends the Amazon S3 Client, allowing you to store data securely in
 * S3.
 *
 * <p>The encryption materials specified in the constructor will be used to encrypt and decrypt
 * data.
 */
public class AmazonS3EncryptionClient extends AmazonS3Client {

  private EncryptionMaterialsProvider encryptionMaterialsProvider;
  private CryptoConfiguration cryptoConfig;

  private static final String USER_AGENT =
      AmazonS3EncryptionClient.class.getName() + "/" + VersionInfoUtils.getVersion();

  /** Shared logger for encryption client events */
  private static Log log = LogFactory.getLog(AmazonS3EncryptionClient.class);

  /** Map of data about in progress encrypted multipart uploads. */
  private Map<String, EncryptedUploadContext> currentMultipartUploadSecretKeys =
      Collections.synchronizedMap(new HashMap<String, EncryptedUploadContext>());

  /**
   * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b> requests to Amazon
   * S3. If {@link #getObject(String, String)} is called, the object contents will be decrypted with
   * the encryption materials provided.
   *
   * <p>Only a subset of the Amazon S3 API will work with anonymous <i>(i.e. unsigned)</i> requests,
   * but this can prove useful in some situations. For example:
   *
   * <ul>
   *   <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the {@link
   *       GroupGrantee#AllUsers} group, anonymous clients can call {@link #listObjects(String)} to
   *       see what objects are stored in a bucket.
   *   <li>If an object has {@link Permission#Read} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can call {@link #getObject(String, String)} and {@link
   *       #getObjectMetadata(String, String)} to pull object content and metadata.
   *   <li>If a bucket has {@link Permission#Write} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can upload objects to the bucket.
   * </ul>
   *
   * @param encryptionMaterials The encryption materials to be used to encrypt and decrypt data.
   */
  public AmazonS3EncryptionClient(EncryptionMaterials encryptionMaterials) {
    this(new StaticEncryptionMaterialsProvider(encryptionMaterials));
  }

  /**
   * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b> requests to Amazon
   * S3. If {@link #getObject(String, String)} is called, the object contents will be decrypted with
   * the encryption materials provided.
   *
   * <p>Only a subset of the Amazon S3 API will work with anonymous <i>(i.e. unsigned)</i> requests,
   * but this can prove useful in some situations. For example:
   *
   * <ul>
   *   <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the {@link
   *       GroupGrantee#AllUsers} group, anonymous clients can call {@link #listObjects(String)} to
   *       see what objects are stored in a bucket.
   *   <li>If an object has {@link Permission#Read} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can call {@link #getObject(String, String)} and {@link
   *       #getObjectMetadata(String, String)} to pull object content and metadata.
   *   <li>If a bucket has {@link Permission#Write} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can upload objects to the bucket.
   * </ul>
   *
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   */
  public AmazonS3EncryptionClient(EncryptionMaterialsProvider encryptionMaterialsProvider) {
    this(
        (AWSCredentialsProvider) null,
        encryptionMaterialsProvider,
        new ClientConfiguration(),
        new CryptoConfiguration());
  }

  /**
   * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b> requests to Amazon
   * S3. If {@link #getObject(String, String)} is called, the object contents will be decrypted with
   * the encryption materials provided. The encryption implementation of the provided crypto
   * provider will be used to encrypt and decrypt data.
   *
   * <p>Only a subset of the Amazon S3 API will work with anonymous <i>(i.e. unsigned)</i> requests,
   * but this can prove useful in some situations. For example:
   *
   * <ul>
   *   <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the {@link
   *       GroupGrantee#AllUsers} group, anonymous clients can call {@link #listObjects(String)} to
   *       see what objects are stored in a bucket.
   *   <li>If an object has {@link Permission#Read} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can call {@link #getObject(String, String)} and {@link
   *       #getObjectMetadata(String, String)} to pull object content and metadata.
   *   <li>If a bucket has {@link Permission#Write} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can upload objects to the bucket.
   * </ul>
   *
   * @param encryptionMaterials The encryption materials to be used to encrypt and decrypt data.
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   */
  public AmazonS3EncryptionClient(
      EncryptionMaterials encryptionMaterials, CryptoConfiguration cryptoConfig) {
    this(new StaticEncryptionMaterialsProvider(encryptionMaterials), cryptoConfig);
  }

  /**
   * Constructs a new Amazon S3 Encryption client that will make <b>anonymous</b> requests to Amazon
   * S3. If {@link #getObject(String, String)} is called, the object contents will be decrypted with
   * the encryption materials provided. The encryption implementation of the provided crypto
   * provider will be used to encrypt and decrypt data.
   *
   * <p>Only a subset of the Amazon S3 API will work with anonymous <i>(i.e. unsigned)</i> requests,
   * but this can prove useful in some situations. For example:
   *
   * <ul>
   *   <li>If an Amazon S3 bucket has {@link Permission#Read} permission for the {@link
   *       GroupGrantee#AllUsers} group, anonymous clients can call {@link #listObjects(String)} to
   *       see what objects are stored in a bucket.
   *   <li>If an object has {@link Permission#Read} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can call {@link #getObject(String, String)} and {@link
   *       #getObjectMetadata(String, String)} to pull object content and metadata.
   *   <li>If a bucket has {@link Permission#Write} permission for the {@link GroupGrantee#AllUsers}
   *       group, anonymous clients can upload objects to the bucket.
   * </ul>
   *
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   */
  public AmazonS3EncryptionClient(
      EncryptionMaterialsProvider encryptionMaterialsProvider, CryptoConfiguration cryptoConfig) {
    this(
        (AWSCredentialsProvider) null,
        encryptionMaterialsProvider,
        new ClientConfiguration(),
        cryptoConfig);
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided.
   *
   * @param credentials The AWS credentials to use when making requests to Amazon S3 with this
   *     client.
   * @param encryptionMaterials The encryption materials to be used to encrypt and decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentials credentials, EncryptionMaterials encryptionMaterials) {
    this(credentials, new StaticEncryptionMaterialsProvider(encryptionMaterials));
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided.
   *
   * @param credentials The AWS credentials to use when making requests to Amazon S3 with this
   *     client.
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentials credentials, EncryptionMaterialsProvider encryptionMaterialsProvider) {
    this(
        credentials,
        encryptionMaterialsProvider,
        new ClientConfiguration(),
        new CryptoConfiguration());
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided.
   *
   * @param credentialsProvider The AWS credentials provider which will provide credentials to
   *     authenticate requests with AWS services.
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentialsProvider credentialsProvider,
      EncryptionMaterialsProvider encryptionMaterialsProvider) {
    this(
        credentialsProvider,
        encryptionMaterialsProvider,
        new ClientConfiguration(),
        new CryptoConfiguration());
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided. The encryption implementation of the provided crypto provider will be used to encrypt
   * and decrypt data.
   *
   * @param credentials The AWS credentials to use when making requests to Amazon S3 with this
   *     client.
   * @param encryptionMaterials The encryption materials to be used to encrypt and decrypt data.
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentials credentials,
      EncryptionMaterials encryptionMaterials,
      CryptoConfiguration cryptoConfig) {
    this(credentials, new StaticEncryptionMaterialsProvider(encryptionMaterials), cryptoConfig);
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided. The encryption implementation of the provided crypto provider will be used to encrypt
   * and decrypt data.
   *
   * @param credentials The AWS credentials to use when making requests to Amazon S3 with this
   *     client.
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentials credentials,
      EncryptionMaterialsProvider encryptionMaterialsProvider,
      CryptoConfiguration cryptoConfig) {
    this(credentials, encryptionMaterialsProvider, new ClientConfiguration(), cryptoConfig);
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials to access
   * Amazon S3. Object contents will be encrypted and decrypted with the encryption materials
   * provided. The encryption implementation of the provided crypto provider will be used to encrypt
   * and decrypt data.
   *
   * @param credentialsProvider The AWS credentials provider which will provide credentials to
   *     authenticate requests with AWS services.
   * @param encryptionMaterialsProvider A provider for the encryption materials to be used to
   *     encrypt and decrypt data.
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   */
  public AmazonS3EncryptionClient(
      AWSCredentialsProvider credentialsProvider,
      EncryptionMaterialsProvider encryptionMaterialsProvider,
      CryptoConfiguration cryptoConfig) {
    this(credentialsProvider, encryptionMaterialsProvider, new ClientConfiguration(), cryptoConfig);
  }

  /**
   * Constructs a new Amazon S3 Encryption client using the specified AWS credentials and client
   * configuration to access Amazon S3. Object contents will be encrypted and decrypted with the
   * encryption materials provided. The crypto provider and storage mode denoted in the specified
   * crypto configuration will be used to encrypt and decrypt data.
   *
   * @param credentials The AWS credentials to use when making requests to Amazon S3 with this
   *     client.
   * @param encryptionMaterials The encryption materials to be used to encrypt and decrypt data.
   * @param clientConfiguration The client configuration options controlling how this client
   *     connects to Amazon S3 (ex: proxy settings, retry counts, etc).
   * @param cryptoConfig The crypto configuration whose parameters will be used to encrypt and
   *     decrypt data.
   * @throws IllegalArgumentException If either of the encryption materials or crypto configuration
   *     parameters are null.
   */
  public AmazonS3EncryptionClient(
      AWSCredentials credentials,
      EncryptionMaterials encryptionMaterials,
      ClientConfiguration clientConfig,
      CryptoConfiguration cryptoConfig) {
    this(
        credentials,
        new StaticEncryptionMaterialsProvider(encryptionMaterials),
        clientConfig,
        cryptoConfig);
  }

  public AmazonS3EncryptionClient(
      AWSCredentials credentials,
      EncryptionMaterialsProvider encryptionMaterialsProvider,
      ClientConfiguration clientConfig,
      CryptoConfiguration cryptoConfig) {
    super(credentials, clientConfig);
    assertParameterNotNull(
        encryptionMaterialsProvider, "EncryptionMaterialsProvider parameter must not be null.");
    assertParameterNotNull(cryptoConfig, "CryptoConfiguration parameter must not be null.");
    this.encryptionMaterialsProvider = encryptionMaterialsProvider;
    this.cryptoConfig = cryptoConfig;
  }

  public AmazonS3EncryptionClient(
      AWSCredentialsProvider credentialsProvider,
      EncryptionMaterialsProvider encryptionMaterialsProvider,
      ClientConfiguration clientConfig,
      CryptoConfiguration cryptoConfig) {
    super(credentialsProvider, clientConfig);
    assertParameterNotNull(
        encryptionMaterialsProvider, "EncryptionMaterialsProvider parameter must not be null.");
    assertParameterNotNull(cryptoConfig, "CryptoConfiguration parameter must not be null.");
    this.encryptionMaterialsProvider = encryptionMaterialsProvider;
    this.cryptoConfig = cryptoConfig;
  }

  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3#putObject(com.amazonaws.services.s3.model.PutObjectRequest)
   */
  @Override
  public PutObjectResult putObject(PutObjectRequest putObjectRequest)
      throws AmazonClientException, AmazonServiceException {

    appendUserAgent(putObjectRequest, USER_AGENT);

    if (this.cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) {
      return putObjectUsingInstructionFile(putObjectRequest);
    } else {
      return putObjectUsingMetadata(putObjectRequest);
    }
  }

  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3#getObject(com.amazonaws.services.s3.model.GetObjectRequest)
   */
  @Override
  public S3Object getObject(GetObjectRequest getObjectRequest)
      throws AmazonClientException, AmazonServiceException {

    appendUserAgent(getObjectRequest, USER_AGENT);

    // Adjust the crypto range to retrieve all of the cipher blocks needed to contain the user's
    // desired
    // range of bytes.
    long[] desiredRange = getObjectRequest.getRange();
    long[] adjustedCryptoRange = EncryptionUtils.getAdjustedCryptoRange(desiredRange);
    if (adjustedCryptoRange != null) {
      getObjectRequest.setRange(adjustedCryptoRange[0], adjustedCryptoRange[1]);
    }

    // Get the object from S3
    S3Object retrievedObject = super.getObject(getObjectRequest);

    // If the caller has specified constraints, it's possible that super.getObject(...)
    // would return null, so we simply return null as well.
    if (retrievedObject == null) return null;

    S3Object objectToBeReturned;
    try {
      // Check if encryption info is in object metadata
      if (EncryptionUtils.isEncryptionInfoInMetadata(retrievedObject)) {
        objectToBeReturned = decryptObjectUsingMetadata(retrievedObject);
      } else {
        // Check if encrypted info is in an instruction file
        S3Object instructionFile = null;
        try {
          instructionFile = getInstructionFile(getObjectRequest);
          if (EncryptionUtils.isEncryptionInfoInInstructionFile(instructionFile)) {
            objectToBeReturned =
                decryptObjectUsingInstructionFile(retrievedObject, instructionFile);
          } else {
            // The object was not encrypted to begin with.  Return the object without decrypting it.
            log.warn(
                String.format(
                    "Unable to detect encryption information for object '%s' in bucket '%s'. "
                        + "Returning object without decryption.",
                    retrievedObject.getKey(), retrievedObject.getBucketName()));
            objectToBeReturned = retrievedObject;
          }
        } finally {
          if (instructionFile != null) {
            try {
              instructionFile.getObjectContent().close();
            } catch (Exception e) {
            }
          }
        }
      }
    } catch (AmazonClientException ace) {
      // If we're unable to set up the decryption, make sure we close the HTTP connection
      try {
        retrievedObject.getObjectContent().close();
      } catch (Exception e) {
      }
      throw ace;
    }

    // Adjust the output to the desired range of bytes.
    return EncryptionUtils.adjustOutputToDesiredRange(objectToBeReturned, desiredRange);
  }

  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3#getObject(com.amazonaws.services.s3.model.GetObjectRequest, java.io.File)
   */
  @Override
  public ObjectMetadata getObject(GetObjectRequest getObjectRequest, File destinationFile)
      throws AmazonClientException, AmazonServiceException {

    assertParameterNotNull(
        destinationFile,
        "The destination file parameter must be specified when downloading an object directly to a file");

    S3Object s3Object = getObject(getObjectRequest);
    // getObject can return null if constraints were specified but not met
    if (s3Object == null) return null;

    OutputStream outputStream = null;
    try {
      outputStream = new BufferedOutputStream(new FileOutputStream(destinationFile));
      byte[] buffer = new byte[1024 * 10];
      int bytesRead;
      while ((bytesRead = s3Object.getObjectContent().read(buffer)) > -1) {
        outputStream.write(buffer, 0, bytesRead);
      }
    } catch (IOException e) {
      throw new AmazonClientException(
          "Unable to store object contents to disk: " + e.getMessage(), e);
    } finally {
      try {
        outputStream.close();
      } catch (Exception e) {
      }
      try {
        s3Object.getObjectContent().close();
      } catch (Exception e) {
      }
    }

    /*
     * Unlike the standard Amazon S3 Client, the Amazon S3 Encryption Client does not do an MD5 check
     * here because the contents stored in S3 and the contents we just retrieved are different.  In
     * S3, the stored contents are encrypted, and locally, the retrieved contents are decrypted.
     */

    return s3Object.getObjectMetadata();
  }

  /* (non-Javadoc)
   * @see com.amazonaws.services.s3.AmazonS3Client#deleteObject(com.amazonaws.services.s3.model.DeleteObjectRequest)
   */
  @Override
  public void deleteObject(DeleteObjectRequest deleteObjectRequest) {

    appendUserAgent(deleteObjectRequest, USER_AGENT);

    // Delete the object
    super.deleteObject(deleteObjectRequest);
    // If it exists, delete the instruction file.
    DeleteObjectRequest instructionDeleteRequest =
        EncryptionUtils.createInstructionDeleteObjectRequest(deleteObjectRequest);
    super.deleteObject(instructionDeleteRequest);
  }

  /* (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;
  }

  /* (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;
  }

  /**
   * {@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;
  }

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

  /*
   * Private helper methods
   */

  /**
   * Puts an encrypted object into S3 and stores encryption info in the object metadata.
   *
   * @param putObjectRequest The request object containing all the parameters to upload a new object
   *     to Amazon S3.
   * @return A {@link PutObjectResult} object containing the information returned by Amazon S3 for
   *     the new, created object.
   * @throws AmazonClientException If any errors are encountered on the client while making the
   *     request or handling the response.
   * @throws AmazonServiceException If any errors occurred in Amazon S3 while processing the
   *     request.
   */
  private PutObjectResult putObjectUsingMetadata(PutObjectRequest putObjectRequest)
      throws AmazonClientException, AmazonServiceException {
    // Create instruction
    EncryptionInstruction instruction =
        EncryptionUtils.generateInstruction(
            this.encryptionMaterialsProvider, this.cryptoConfig.getCryptoProvider());

    // Encrypt the object data with the instruction
    PutObjectRequest encryptedObjectRequest =
        EncryptionUtils.encryptRequestUsingInstruction(putObjectRequest, instruction);

    // Update the metadata
    EncryptionUtils.updateMetadataWithEncryptionInstruction(putObjectRequest, instruction);

    // Put the encrypted object into S3
    return super.putObject(encryptedObjectRequest);
  }

  /**
   * Puts an encrypted object into S3, and puts an instruction file into S3. Encryption info is
   * stored in the instruction file.
   *
   * @param putObjectRequest The request object containing all the parameters to upload a new object
   *     to Amazon S3.
   * @return A {@link PutObjectResult} object containing the information returned by Amazon S3 for
   *     the new, created object.
   * @throws AmazonClientException If any errors are encountered on the client while making the
   *     request or handling the response.
   * @throws AmazonServiceException If any errors occurred in Amazon S3 while processing the
   *     request.
   */
  private PutObjectResult putObjectUsingInstructionFile(PutObjectRequest putObjectRequest)
      throws AmazonClientException, AmazonServiceException {
    // Create instruction
    EncryptionInstruction instruction =
        EncryptionUtils.generateInstruction(
            this.encryptionMaterialsProvider, this.cryptoConfig.getCryptoProvider());

    // Encrypt the object data with the instruction
    PutObjectRequest encryptedObjectRequest =
        EncryptionUtils.encryptRequestUsingInstruction(putObjectRequest, instruction);

    // Put the encrypted object into S3
    PutObjectResult encryptedObjectResult = super.putObject(encryptedObjectRequest);

    // Put the instruction file into S3
    PutObjectRequest instructionRequest =
        EncryptionUtils.createInstructionPutRequest(putObjectRequest, instruction);
    super.putObject(instructionRequest);

    // Return the result of the encrypted object PUT.
    return encryptedObjectResult;
  }

  /**
   * Decrypts an object using information retrieved from metadata. If decryption is not possible,
   * returns null.
   *
   * @param object The S3Object to be decrypted.
   * @return An S3Object with decrypted object contents. If decryption is not possible, returns
   *     null.
   */
  private S3Object decryptObjectUsingMetadata(S3Object object) {
    // Create an instruction object from the object headers
    EncryptionInstruction instruction =
        EncryptionUtils.buildInstructionFromObjectMetadata(
            object, this.encryptionMaterialsProvider, this.cryptoConfig.getCryptoProvider());

    // Decrypt the object file with the instruction
    return EncryptionUtils.decryptObjectUsingInstruction(object, instruction);
  }

  /**
   * Decrypts an object using information retrieved from an instruction file.
   *
   * @param object The S3Object to be decrypted.
   * @param instructionFile The S3Object instruction file to be used to decrypt the object.
   * @return An S3Object with decrypted object contents.
   */
  private S3Object decryptObjectUsingInstructionFile(S3Object object, S3Object instructionFile) {
    // Create an instruction object from the retrieved instruction file
    EncryptionInstruction instruction =
        EncryptionUtils.buildInstructionFromInstructionFile(
            instructionFile,
            this.encryptionMaterialsProvider,
            this.cryptoConfig.getCryptoProvider());

    // Decrypt the object file with the instruction
    return EncryptionUtils.decryptObjectUsingInstruction(object, instruction);
  }

  /**
   * Retrieves an instruction file from S3. If no instruction file is found, returns null.
   *
   * @param getObjectRequest A GET request for an object in S3. The parameters from this request
   *     will be used to retrieve the corresponding instruction file.
   * @return An instruction file, or null if no instruction file was found.
   */
  private S3Object getInstructionFile(GetObjectRequest getObjectRequest) {
    try {
      GetObjectRequest instructionFileRequest =
          EncryptionUtils.createInstructionGetRequest(getObjectRequest);
      return super.getObject(instructionFileRequest);
    } catch (AmazonServiceException e) {
      // If no instruction file is found, log a debug message, and return null.
      log.debug("Unable to retrieve instruction file : " + e.getMessage());
      return null;
    }
  }

  /**
   * Asserts that the specified parameter value is not null and if it is, throws an
   * IllegalArgumentException with the specified error message.
   *
   * @param parameterValue The parameter value being checked.
   * @param errorMessage The error message to include in the IllegalArgumentException if the
   *     specified parameter is null.
   */
  private void assertParameterNotNull(Object parameterValue, String errorMessage) {
    if (parameterValue == null) throw new IllegalArgumentException(errorMessage);
  }

  public <X extends AmazonWebServiceRequest> X appendUserAgent(X request, String userAgent) {
    request.getRequestClientOptions().appendUserAgent(userAgent);
    return request;
  }
}