All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.s3.internal.crypto.S3CryptoModuleAE Maven / Gradle / Ivy

There is a newer version: 2.9.1
Show newest version
/*
 * Copyright 2013-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.amazonaws.services.s3.internal.crypto;

import static com.amazonaws.services.s3.AmazonS3EncryptionClient.USER_AGENT;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.createInstructionGetRequest;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.getAdjustedCryptoRange;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.s3.internal.InputSubstream;
import com.amazonaws.services.s3.internal.RepeatableFileInputStream;
import com.amazonaws.services.s3.internal.S3Direct;
import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest;
import com.amazonaws.services.s3.model.CompleteMultipartUploadResult;
import com.amazonaws.services.s3.model.CopyPartRequest;
import com.amazonaws.services.s3.model.CopyPartResult;
import com.amazonaws.services.s3.model.CryptoConfiguration;
import com.amazonaws.services.s3.model.CryptoStorageMode;
import com.amazonaws.services.s3.model.EncryptionMaterialsProvider;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadResult;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.PutObjectResult;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.amazonaws.services.s3.model.UploadPartRequest;
import com.amazonaws.services.s3.model.UploadPartResult;
import com.amazonaws.util.json.JsonUtils;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

/**
 * Authenticated encryption (AE) cryptographic module for the S3 encryption
 * client.
 */
class S3CryptoModuleAE extends S3CryptoModuleBase {
    static {
        // Enable bouncy castle if available
        CryptoRuntime.enableBouncyCastle();
    }
    private static final boolean IS_MULTI_PART = true;

    S3CryptoModuleAE(S3Direct s3,
            AWSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            ClientConfiguration clientConfig,
            CryptoConfiguration cryptoConfig) {
        super(s3, credentialsProvider, encryptionMaterialsProvider,
                clientConfig, cryptoConfig,
                new S3CryptoScheme(ContentCryptoScheme.AES_GCM));
    }

    /**
     * Used for testing purposes only.
     */
    S3CryptoModuleAE(S3Direct s3,
            EncryptionMaterialsProvider encryptionMaterialsProvider,
            CryptoConfiguration cryptoConfig) {
        this(s3, new DefaultAWSCredentialsProviderChain(),
                encryptionMaterialsProvider, new ClientConfiguration(),
                cryptoConfig);
    }

    /**
     * Returns true if a strict encryption mode is in use in the current crypto
     * module; false otherwise.
     */
    protected boolean isStrict() {
        return false;
    }

    /**
     * @throws SecurityException if the crypto scheme used in the given content
     *             crypto material is not allowed in this crypto module.
     */
    protected void securityCheck(ContentCryptoMaterial cekMaterial,
            S3ObjectWrapper retrieved) {
        // default is no-op. Sublcass may override.
    }

    @Override
    public PutObjectResult putObjectSecurely(PutObjectRequest putObjectRequest)
            throws AmazonClientException, AmazonServiceException {
        appendUserAgent(putObjectRequest, USER_AGENT);

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

    private PutObjectResult putObjectUsingMetadata(PutObjectRequest req)
            throws AmazonClientException, AmazonServiceException {
        ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(req);
        // Wraps the object data with a cipher input stream
        PutObjectRequest wrappedReq = wrapWithCipher(req, cekMaterial);
        // Update the metadata
        req.setMetadata(updateMetadataWithContentCryptoMaterial(
                req.getMetadata(), req.getFile(),
                cekMaterial));
        // Put the encrypted object into S3
        return s3.putObject(wrappedReq);
    }

    @Override
    public S3Object getObjectSecurely(GetObjectRequest req)
            throws AmazonClientException, AmazonServiceException {
        appendUserAgent(req, 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 = req.getRange();
        if (isStrict() && desiredRange != null)
            throw new SecurityException("Range get is not allowed in strict crypto mode");
        long[] adjustedCryptoRange = getAdjustedCryptoRange(desiredRange);
        if (adjustedCryptoRange != null)
            req.setRange(adjustedCryptoRange[0], adjustedCryptoRange[1]);
        // Get the object from S3
        S3Object retrieved = s3.getObject(req);
        // If the caller has specified constraints, it's possible that
        // super.getObject(...)
        // would return null, so we simply return null as well.
        if (retrieved == null)
            return null;
        try {
            return decipher(req, desiredRange, adjustedCryptoRange, retrieved);
        } catch (AmazonClientException ace) {
            // If we're unable to set up the decryption, make sure we close the
            // HTTP connection
            try {
                retrieved.getObjectContent().close();
            } catch (Exception e) {
                log.debug("Safely ignoring", e);
            }
            throw ace;
        }
    }

    private S3Object decipher(GetObjectRequest req,
            long[] desiredRange, long[] cryptoRange,
            S3Object retrieved) {
        S3ObjectWrapper wrapped = new S3ObjectWrapper(retrieved);
        // Check if encryption info is in object metadata
        if (wrapped.hasEncryptionInfo())
            return decipherWithMetadata(desiredRange, cryptoRange, wrapped);
        // Check if encrypted info is in an instruction file
        S3ObjectWrapper instructionFile = fetchInstructionFile(req);
        if (instructionFile != null) {
            try {
                if (instructionFile.isInstructionFile()) {
                    return decipherWithInstructionFile(desiredRange, cryptoRange,
                            wrapped, instructionFile);
                }
            } finally {
                try {
                    instructionFile.getObjectContent().close();
                } catch (Exception ignore) {
                }
            }
        }
        if (isStrict()) {
            try {
                wrapped.close();
            } catch (IOException ignore) {
            }
            throw new SecurityException("S3 object with bucket name: "
                    + retrieved.getBucketName() + ", key: "
                    + retrieved.getKey() + " is not encrypted");
        }
        // 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.",
                retrieved.getKey(),
                retrieved.getBucketName()));
        // Adjust the output to the desired range of bytes.
        S3ObjectWrapper adjusted = adjustToDesiredRange(wrapped, desiredRange, null);
        return adjusted.getS3Object();
    }

    private S3Object decipherWithInstructionFile(long[] desiredRange,
            long[] cryptoRange, S3ObjectWrapper retrieved,
            S3ObjectWrapper instructionFile) {
        String json = instructionFile.toJsonString();
        Map instruction = JsonUtils.jsonToMap(json);
        ContentCryptoMaterial cekMaterial =
                ContentCryptoMaterial.fromInstructionFile(
                        instruction,
                        kekMaterialsProvider,
                        cryptoConfig.getCryptoProvider(),
                        cryptoRange // range is sometimes necessary to compute
                                    // the adjusted IV
                        );
        securityCheck(cekMaterial, retrieved);
        S3ObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange);
        // Adjust the output to the desired range of bytes.
        S3ObjectWrapper adjusted = adjustToDesiredRange(
                decrypted, desiredRange, instruction);
        return adjusted.getS3Object();
    }

    private S3Object decipherWithMetadata(long[] desiredRange,
            long[] cryptoRange, S3ObjectWrapper retrieved) {
        ContentCryptoMaterial cekMaterial = ContentCryptoMaterial
                .fromObjectMetadata(retrieved.getObjectMetadata(),
                        kekMaterialsProvider,
                        cryptoConfig.getCryptoProvider(),
                        // range is sometimes necessary to compute the adjusted
                        // IV
                        cryptoRange
                );
        securityCheck(cekMaterial, retrieved);
        S3ObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange);
        // Adjust the output to the desired range of bytes.
        S3ObjectWrapper adjusted = adjustToDesiredRange(
                decrypted, desiredRange, null);
        return adjusted.getS3Object();
    }

    /**
     * Adjusts the retrieved S3Object so that the object contents contain only
     * the range of bytes desired by the user. Since encrypted contents can only
     * be retrieved in CIPHER_BLOCK_SIZE (16 bytes) chunks, the S3Object
     * potentially contains more bytes than desired, so this method adjusts the
     * contents range.
     *
     * @param s3object The S3Object retrieved from S3 that could possibly
     *            contain more bytes than desired by the user.
     * @param range A two-element array of longs corresponding to the start and
     *            finish (inclusive) of a desired range of bytes.
     * @param instruction Instruction file in JSON or null if no instruction
     *            file is involved
     * @return The S3Object with adjusted object contents containing only the
     *         range desired by the user. If the range specified is invalid,
     *         then the S3Object is returned without any modifications.
     */
    protected final S3ObjectWrapper adjustToDesiredRange(S3ObjectWrapper s3object,
            long[] range, Map instruction) {
        if (range == null)
            return s3object;
        // Figure out the original encryption scheme used, which can be
        // different from the crypto scheme used for decryption.
        ContentCryptoScheme encryptionScheme = s3object.encryptionSchemeOf(instruction);
        // range get on data encrypted using AES_GCM
        final long instanceLen = s3object.getObjectMetadata().getInstanceLength();
        final long maxOffset = instanceLen - encryptionScheme.getTagLengthInBits() / 8 - 1;
        if (range[1] > maxOffset) {
            range[1] = maxOffset;
            if (range[0] > range[1]) {
                // Return empty content
                try { // First let's close the existing input stream to
                      // avoid resource leakage
                    s3object.getObjectContent().close();
                } catch (IOException ignore) {
                    log.trace("", ignore);
                }
                s3object.setObjectContent(new ByteArrayInputStream(new byte[0]));
                return s3object;
            }
        }
        if (range[0] > range[1]) {
            // Make no modifications if range is invalid.
            return s3object;
        }
        try {
            S3ObjectInputStream objectContent = s3object.getObjectContent();
            InputStream adjustedRangeContents = new AdjustedRangeInputStream(objectContent,
                    range[0], range[1]);
            s3object.setObjectContent(new S3ObjectInputStream(adjustedRangeContents, objectContent
                    .getHttpRequest()));
            return s3object;
        } catch (IOException e) {
            throw new AmazonClientException("Error adjusting output to desired byte range: "
                    + e.getMessage());
        }
    }

    @Override
    public ObjectMetadata getObjectSecurely(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 = getObjectSecurely(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) {
                log.debug(e.getMessage());
            }
            try {
                s3Object.getObjectContent().close();
            } catch (Exception e) {
                log.debug(e.getMessage());
            }
        }

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

    @Override
    public CompleteMultipartUploadResult completeMultipartUploadSecurely(
            CompleteMultipartUploadRequest req) throws AmazonClientException,
            AmazonServiceException {
        appendUserAgent(req, USER_AGENT);
        String uploadId = req.getUploadId();
        MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId);

        if (uploadContext.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 = s3.completeMultipartUpload(req);

        // In InstructionFile mode, we want to write the instruction file only
        // after the whole upload has completed correctly.
        if (cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) {
            // Put the instruction file into S3
            s3.putObject(createInstructionPutRequest(
                    uploadContext.getBucketName(),
                    uploadContext.getKey(),
                    uploadContext.getContentCryptoMaterial()));
        }
        multipartUploadContexts.remove(uploadId);
        return result;
    }

    @Override
    public InitiateMultipartUploadResult initiateMultipartUploadSecurely(
            InitiateMultipartUploadRequest req)
            throws AmazonClientException, AmazonServiceException {
        appendUserAgent(req, USER_AGENT);
        // Generate a one-time use symmetric key and initialize a cipher to
        // encrypt object data
        ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(req);
        if (cryptoConfig.getStorageMode() == CryptoStorageMode.ObjectMetadata) {
            ObjectMetadata metadata = req.getObjectMetadata();
            if (metadata == null)
                metadata = new ObjectMetadata();
            // Store encryption info in metadata
            req.setObjectMetadata(updateMetadataWithContentCryptoMaterial(
                    metadata, null, cekMaterial));
        }
        InitiateMultipartUploadResult result = s3.initiateMultipartUpload(req);
        MultipartUploadCryptoContext uploadContext = new MultipartUploadCryptoContext(
                req.getBucketName(), req.getKey(), cekMaterial);
        multipartUploadContexts.put(result.getUploadId(), uploadContext);
        return result;
    }

    /**
     * {@inheritDoc}
     * 

* NOTE: Because the encryption process requires context from * previous blocks, 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 uploadPartSecurely(UploadPartRequest req) throws AmazonClientException, AmazonServiceException { appendUserAgent(req, USER_AGENT); final int blockSize = contentCryptoScheme.getBlockSizeInBytes(); final boolean isLastPart = req.isLastPart(); final String uploadId = req.getUploadId(); final long partSize = req.getPartSize(); final boolean partSizeMultipleOfCipherBlockSize = 0 == (partSize % blockSize); if (!isLastPart && !partSizeMultipleOfCipherBlockSize) { throw new AmazonClientException( "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."); } // Generate the envelope symmetric key and initialize a cipher to // encrypt the object's data MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId); if (uploadContext == null) { throw new AmazonClientException( "No client-side information available on upload ID " + uploadId); } CipherLite cipherLite = uploadContext.getCipherLite(); req.setInputStream(newMultipartS3CipherInputStream(req, cipherLite)); // Treat all encryption requests as input stream upload requests, not as // file upload requests. req.setFile(null); req.setFileOffset(0); // The last part of the multipart upload will contain an extra 16-byte // mac if (req.isLastPart()) { // We only change the size of the last part req.setPartSize(partSize + (contentCryptoScheme.getTagLengthInBits() / 8)); if (uploadContext.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."); } uploadContext.setHasFinalPartBeenSeen(true); } UploadPartResult result = s3.uploadPart(req); return result; } protected final CipherLiteInputStream newMultipartS3CipherInputStream( UploadPartRequest req, CipherLite cipherLite) { try { InputStream is = req.getInputStream(); if (req.getFile() != null) { is = new InputSubstream( new RepeatableFileInputStream( req.getFile()), req.getFileOffset(), req.getPartSize(), req.isLastPart()); } return new CipherLiteInputStream(is, cipherLite, DEFAULT_BUFFER_SIZE, IS_MULTI_PART, req.isLastPart()); } catch (Exception e) { throw new AmazonClientException( "Unable to create cipher input stream: " + e.getMessage(), e); } } @Override public CopyPartResult copyPartSecurely(CopyPartRequest copyPartRequest) { String uploadId = copyPartRequest.getUploadId(); MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId); if (!uploadContext.hasFinalPartBeenSeen()) { uploadContext.setHasFinalPartBeenSeen(true); } return s3.copyPart(copyPartRequest); } /* * Private helper methods */ /** * 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 { PutObjectRequest putInstFileRequest = putObjectRequest.clone(); // Create instruction ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(putObjectRequest); // Wraps the object data with a cipher input stream; note the metadata // is mutated as a side effect. PutObjectRequest req = wrapWithCipher(putObjectRequest, cekMaterial); // Put the encrypted object into S3 PutObjectResult result = s3.putObject(req); // Put the instruction file into S3 s3.putObject(upateInstructionPutRequest(putInstFileRequest, cekMaterial)); // Return the result of the encrypted object PUT. return result; } /** * Returns an updated object where the object content input stream contains * the decrypted contents. * * @param wrapper The object whose contents are to be decrypted. * @param cekMaterial The instruction that will be used to decrypt the * object data. * @return The updated object where the object content input stream contains * the decrypted contents. */ private S3ObjectWrapper decrypt(S3ObjectWrapper wrapper, ContentCryptoMaterial cekMaterial, long[] range) { S3ObjectInputStream objectContent = wrapper.getObjectContent(); wrapper.setObjectContent(new S3ObjectInputStream( new CipherLiteInputStream(objectContent, cekMaterial .getCipherLite(), DEFAULT_BUFFER_SIZE), objectContent .getHttpRequest())); return wrapper; } /** * 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 S3ObjectWrapper fetchInstructionFile(GetObjectRequest getObjectRequest) { try { S3Object o = s3.getObject(createInstructionGetRequest(getObjectRequest)); return o == null ? null : new S3ObjectWrapper(o); } 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); } @Override protected final long ciphertextLength(long originalContentLength) { // Add 16 bytes for the 128-bit tag length using AES/GCM return originalContentLength + contentCryptoScheme.getTagLengthInBits() / 8; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy