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

com.qcloud.cos.internal.crypto.COSCryptoModuleAE Maven / Gradle / Ivy

/*
 * Copyright 2010-2019 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.
 
 * According to cos feature, we modify some class,comment, field name, etc.
 */


package com.qcloud.cos.internal.crypto;

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.Collections;
import java.util.Map;

import com.qcloud.cos.auth.COSCredentialsProvider;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.internal.COSDirect;
import com.qcloud.cos.model.COSObject;
import com.qcloud.cos.model.COSObjectId;
import com.qcloud.cos.model.COSObjectInputStream;
import com.qcloud.cos.model.EncryptedGetObjectRequest;
import com.qcloud.cos.model.GetObjectRequest;
import com.qcloud.cos.model.InitiateMultipartUploadRequest;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.UploadPartRequest;
import com.qcloud.cos.utils.IOUtils;
import com.qcloud.cos.utils.Jackson;

public class COSCryptoModuleAE extends COSCryptoModuleBase {

    static {
        // Enable bouncy castle if available
        CryptoRuntime.enableBouncyCastle();
    }

    public COSCryptoModuleAE(COSDirect cos, COSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider kekMaterialsProvider, CryptoConfiguration cryptoConfig) {
        this(null, cos, credentialsProvider, kekMaterialsProvider, cryptoConfig);
    }

    public COSCryptoModuleAE(QCLOUDKMS kms, COSDirect cos,
            COSCredentialsProvider credentialsProvider,
            EncryptionMaterialsProvider kekMaterialsProvider, CryptoConfiguration cryptoConfig) {
        super(kms, cos, credentialsProvider, kekMaterialsProvider, cryptoConfig);
    }

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

    @Override
    public COSObject getObjectSecurely(GetObjectRequest req) {
        // 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 and getting a part are not allowed in strict crypto mode");
        long[] adjustedCryptoRange = getAdjustedCryptoRange(desiredRange);
        if (adjustedCryptoRange != null)
            req.setRange(adjustedCryptoRange[0], adjustedCryptoRange[1]);
        // Get the object from COS
        COSObject retrieved = cos.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;
        String suffix = null;
        if (req instanceof EncryptedGetObjectRequest) {
            EncryptedGetObjectRequest ereq = (EncryptedGetObjectRequest) req;
            suffix = ereq.getInstructionFileSuffix();
        }
        try {
            return suffix == null || suffix.trim().isEmpty()
                    ? decipher(req, desiredRange, adjustedCryptoRange, retrieved)
                    : decipherWithInstFileSuffix(req, desiredRange, adjustedCryptoRange, retrieved,
                            suffix);
        } catch (RuntimeException ex) {
            // If we're unable to set up the decryption, make sure we close the
            // HTTP connection
            IOUtils.closeQuietly(retrieved, log);
            throw ex;
        } catch (Error error) {
            IOUtils.closeQuietly(retrieved, log);
            throw error;
        }
    }

    private COSObject decipher(GetObjectRequest req, long[] desiredRange, long[] cryptoRange,
            COSObject retrieved) {
        COSObjectWrapper wrapped = new COSObjectWrapper(retrieved, req.getCOSObjectId());
        // Check if encryption info is in object metadata
        if (wrapped.hasEncryptionInfo())
            return decipherWithMetadata(req, desiredRange, cryptoRange, wrapped);
        // Check if encrypted info is in an instruction file
        COSObjectWrapper ifile = fetchInstructionFile(req.getCOSObjectId(), null);
        if (ifile != null) {
            try {
                return decipherWithInstructionFile(req, desiredRange, cryptoRange, wrapped, ifile);
            } finally {
                IOUtils.closeQuietly(ifile, log);
            }
        }

        if (isStrict() || !cryptoConfig.isIgnoreMissingInstructionFile()) {
            IOUtils.closeQuietly(wrapped, log);
            throw new SecurityException(
                    "Instruction file not found for COS object with bucket name: "
                            + retrieved.getBucketName() + ", key: " + retrieved.getKey());
        }
        // To keep backward compatible:
        // ignore the missing instruction file and treat the object as un-encrypted.
        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.
        COSObjectWrapper adjusted = adjustToDesiredRange(wrapped, desiredRange, null);
        return adjusted.getCOSObject();
    }

    /**
     * Same as {@link #decipher(GetObjectRequest, long[], long[], COSObject)} but makes use of an
     * instruction file with the specified suffix.
     * 
     * @param instFileSuffix never null or empty (which is assumed to have been sanitized upstream.)
     */
    private COSObject decipherWithInstFileSuffix(GetObjectRequest req, long[] desiredRange,
            long[] cryptoRange, COSObject retrieved, String instFileSuffix) {
        final COSObjectId id = req.getCOSObjectId();
        // Check if encrypted info is in an instruction file
        final COSObjectWrapper ifile = fetchInstructionFile(id, instFileSuffix);
        if (ifile == null) {
            throw new CosClientException("Instruction file with suffix " + instFileSuffix
                    + " is not found for " + retrieved);
        }
        try {
            return decipherWithInstructionFile(req, desiredRange, cryptoRange,
                    new COSObjectWrapper(retrieved, id), ifile);
        } finally {
            IOUtils.closeQuietly(ifile, log);
        }
    }

    private COSObject decipherWithInstructionFile(GetObjectRequest req, long[] desiredRange,
            long[] cryptoRange, COSObjectWrapper retrieved, COSObjectWrapper instructionFile) {
        boolean keyWrapExpected = isStrict();
        if (req instanceof EncryptedGetObjectRequest) {
            EncryptedGetObjectRequest ereq = (EncryptedGetObjectRequest) req;
            if (!keyWrapExpected)
                keyWrapExpected = ereq.isKeyWrapExpected();
        }
        String json = instructionFile.toJsonString();
        @SuppressWarnings("unchecked")
        Map matdesc =
                Collections.unmodifiableMap(Jackson.fromJsonString(json, Map.class));
        ContentCryptoMaterial cekMaterial = ContentCryptoMaterial.fromInstructionFile(matdesc,
                kekMaterialsProvider, cryptoConfig.getCryptoProvider(), cryptoRange, // range is
                                                                                     // sometimes
                                                                                     // necessary to
                                                                                     // compute the
                                                                                     // adjusted IV
                keyWrapExpected, kms);
        securityCheck(cekMaterial, retrieved);
        COSObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange);
        // Adjust the output to the desired range of bytes.
        COSObjectWrapper adjusted = adjustToDesiredRange(decrypted, desiredRange, matdesc);
        return adjusted.getCOSObject();
    }

    private COSObject decipherWithMetadata(GetObjectRequest req, long[] desiredRange,
            long[] cryptoRange, COSObjectWrapper retrieved) {
        boolean keyWrapExpected = isStrict();
        if (req instanceof EncryptedGetObjectRequest) {
            EncryptedGetObjectRequest ereq = (EncryptedGetObjectRequest) req;
            if (!keyWrapExpected)
                keyWrapExpected = ereq.isKeyWrapExpected();
        }
        ContentCryptoMaterial cekMaterial =
                ContentCryptoMaterial.fromObjectMetadata(retrieved.getObjectMetadata(),
                        kekMaterialsProvider, cryptoConfig.getCryptoProvider(),
                        // range is sometimes necessary to compute the adjusted IV
                        cryptoRange, keyWrapExpected, kms);
        securityCheck(cekMaterial, retrieved);
        COSObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange);
        // Adjust the output to the desired range of bytes.
        COSObjectWrapper adjusted = adjustToDesiredRange(decrypted, desiredRange, null);
        return adjusted.getCOSObject();
    }

    /**
     * Adjusts the retrieved COSObject 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 COSObject potentially contains more bytes than desired, so this method
     * adjusts the contents range.
     *
     * @param cosObject The COSObject retrieved from COS 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 COSObject with adjusted object contents containing only the range desired by the
     *         user. If the range specified is invalid, then the COSObject is returned without any
     *         modifications.
     */
    protected final COSObjectWrapper adjustToDesiredRange(COSObjectWrapper cosObject, long[] range,
            Map instruction) {
        if (range == null)
            return cosObject;
        // Figure out the original encryption scheme used, which can be
        // different from the crypto scheme used for decryption.
        ContentCryptoScheme encryptionScheme = cosObject.encryptionSchemeOf(instruction);
        // range get on data encrypted using AES_GCM
        final long instanceLen = cosObject.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
                // First let's close the existing input stream to avoid resource
                // leakage
                IOUtils.closeQuietly(cosObject.getObjectContent(), log);
                cosObject.setObjectContent(new ByteArrayInputStream(new byte[0]));
                return cosObject;
            }
        }
        if (range[0] > range[1]) {
            // Make no modifications if range is invalid.
            return cosObject;
        }
        try {
            COSObjectInputStream objectContent = cosObject.getObjectContent();
            InputStream adjustedRangeContents =
                    new AdjustedRangeInputStream(objectContent, range[0], range[1]);
            cosObject.setObjectContent(new COSObjectInputStream(adjustedRangeContents,
                    objectContent.getHttpRequest()));
            return cosObject;
        } catch (IOException e) {
            throw new CosClientException(
                    "Error adjusting output to desired byte range: " + e.getMessage());
        }
    }

    @Override
    public ObjectMetadata getObjectSecurely(GetObjectRequest getObjectRequest,
            File destinationFile) {
        assertParameterNotNull(destinationFile,
                "The destination file parameter must be specified when downloading an object directly to a file");

        COSObject cosObject = getObjectSecurely(getObjectRequest);
        // getObject can return null if constraints were specified but not met
        if (cosObject == null)
            return null;

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

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

        return cosObject.getObjectMetadata();
    }

    @Override
    final MultipartUploadCryptoContext newUploadContext(InitiateMultipartUploadRequest req,
            ContentCryptoMaterial cekMaterial) {
        return new MultipartUploadCryptoContext(req.getBucketName(), req.getKey(), cekMaterial);
    }

    //// specific overrides for uploading parts.
    @Override
    final CipherLite cipherLiteForNextPart(MultipartUploadCryptoContext uploadContext) {
        return uploadContext.getCipherLite();
    }

    @Override
    final long computeLastPartSize(UploadPartRequest req) {
        return req.getPartSize() + (contentCryptoScheme.getTagLengthInBits() / 8);
    }

    /*
     * Private helper methods
     */

    /**
     * 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 COSObjectWrapper decrypt(COSObjectWrapper wrapper, ContentCryptoMaterial cekMaterial,
            long[] range) {
        COSObjectInputStream objectContent = wrapper.getObjectContent();
        wrapper.setObjectContent(new COSObjectInputStream(new CipherLiteInputStream(objectContent,
                cekMaterial.getCipherLite(), DEFAULT_BUFFER_SIZE), objectContent.getHttpRequest()));
        return wrapper;
    }

    /**
     * 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 long ciphertextLength(long originalContentLength) {
        // Add 16 bytes for the 128-bit tag length using AES/GCM
        return originalContentLength + contentCryptoScheme.getTagLengthInBits() / 8;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy