com.microsoft.azure.storage.blob.BlobEncryptionPolicy.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.azure.storage.blob.BlobEncryptionPolicy.java

Source

/**
 * Copyright Microsoft Corporation
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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.microsoft.azure.storage.blob;

import java.io.OutputStream;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang3.tuple.Pair;

import com.microsoft.azure.keyvault.core.IKey;
import com.microsoft.azure.keyvault.core.IKeyResolver;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.EncryptionAgent;
import com.microsoft.azure.storage.core.EncryptionAlgorithm;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;
import com.microsoft.azure.storage.core.WrappedContentKey;

/**
 * Represents a blob encryption policy that is used to perform envelope encryption/decryption of Azure blobs.
 */
public final class BlobEncryptionPolicy {

    /**
     * The {@link IKeyResolver} used to select the correct key for decrypting existing blobs.
     */
    public IKeyResolver keyResolver;

    /**
     * An object of type {@link IKey} that is used to wrap/unwrap the content key during encryption.
     */
    public IKey keyWrapper;

    /**
     * Initializes a new instance of the {@link BlobEncryptionPolicy} class with the specified key and resolver.
     * <p>
     * If the generated policy is intended to be used for encryption, users are expected to provide a key at the
     * minimum. The absence of key will cause an exception to be thrown during encryption. If the generated policy is
     * intended to be used for decryption, users can provide a keyResolver. The client library will - 1. Invoke the key
     * resolver if specified to get the key. 2. If resolver is not specified but a key is specified, match the key id on
     * the key and use it.
     * 
     * @param key
     *            An object of type {@link IKey} that is used to wrap/unwrap the content encryption key.
     * @param keyResolver
     *            The key resolver used to select the correct key for decrypting existing blobs.
     */
    public BlobEncryptionPolicy(IKey key, IKeyResolver keyResolver) {
        this.keyWrapper = key;
        this.keyResolver = keyResolver;
    }

    /**
     * Gets the {@link IKey} that is used to wrap/unwrap the content key during encryption.
     * 
     * @return An {@link IKey} object.
     */
    public IKey getKey() {
        return this.keyWrapper;
    }

    /**
     * Gets the key resolver used to select the correct key for decrypting existing blobs.
     * 
     * @return A resolver that returns an {@link IKey} given a keyId.
     */
    public IKeyResolver getKeyResolver() {
        return this.keyResolver;
    }

    /**
     * Sets the {@link IKey} that is used to wrap/unwrap the content key during encryption.
     * 
     * @param key
     *            An {@link IKey} object.
     */
    public void setKey(IKey key) {
        this.keyWrapper = key;
    }

    /**
     * Sets the key resolver used to select the correct key for decrypting existing blobs.
     * 
     * @param keyResolver
     *            A resolver that returns an {@link IKey} given a keyId.
     */
    public void setKeyResolver(IKeyResolver keyResolver) {
        this.keyResolver = keyResolver;
    }

    /**
     * Return a reference to a {@link OutputStream} given a user stream. This method is used for decrypting blobs.
     * @param userProvidedStream
     *          The output stream provided by the user.
     * @param metadata
     *          Reference to blob metadata object that is used to get the encryption materials.
     * @param requireEncryption
     *          A value to indicate that the data read from the server should be encrypted.
     * @param iv
     *          The iv to use if pre-buffered. Used only for range reads.
     * @param noPadding
     *          Value indicating if the padding mode should be set or not.
     * @return A reference to a {@link OutputStream} that will be written to.
     * @throws StorageException
     *             An exception representing any error which occurred during the operation.
     */
    OutputStream decryptBlob(OutputStream userProvidedStream, Map<String, String> metadata,
            Boolean requireEncryption, byte[] iv, boolean noPadding) throws StorageException {
        Utility.assertNotNull("metadata", metadata);

        // If encryption policy is set but the encryption metadata is absent, throw
        // an exception.
        String encryptionDataString = metadata.get("encryptiondata");
        if (requireEncryption != null && requireEncryption && encryptionDataString == null) {
            throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
                    SR.ENCRYPTION_DATA_NOT_PRESENT_ERROR, null);
        }

        try {
            if (encryptionDataString != null) {
                BlobEncryptionData encryptionData = BlobEncryptionData.deserialize(encryptionDataString);

                Utility.assertNotNull("encryptionData", encryptionData);
                Utility.assertNotNull("contentEncryptionIV", encryptionData.getContentEncryptionIV());
                Utility.assertNotNull("encryptedKey", encryptionData.getWrappedContentKey().getEncryptedKey());

                // Throw if the encryption protocol on the message doesn't match the version that this client library understands
                // and is able to decrypt.
                if (!Constants.EncryptionConstants.ENCRYPTION_PROTOCOL_V1
                        .equals(encryptionData.getEncryptionAgent().getProtocol())) {
                    throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
                            SR.ENCRYPTION_PROTOCOL_VERSION_INVALID, null);
                }

                // Throw if neither the key nor the key resolver are set.
                if (this.keyWrapper == null && this.keyResolver == null) {
                    throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
                            SR.KEY_AND_RESOLVER_MISSING, null);
                }

                byte[] contentEncryptionKey = null;

                // 1. Invoke the key resolver if specified to get the key. If the resolver is specified but does not have a
                // mapping for the key id, an error should be thrown. This is important for key rotation scenario.
                // 2. If resolver is not specified but a key is specified, match the key id on the key and and use it.
                // Calling UnwrapKeyAsync synchronously is fine because for the storage client scenario, unwrap happens
                // locally. No service call is made.
                if (this.keyResolver != null) {
                    IKey keyEncryptionKey = this.keyResolver
                            .resolveKeyAsync(encryptionData.getWrappedContentKey().getKeyId()).get();

                    Utility.assertNotNull("keyEncryptionKey", keyEncryptionKey);
                    contentEncryptionKey = keyEncryptionKey
                            .unwrapKeyAsync(encryptionData.getWrappedContentKey().getEncryptedKey(),
                                    encryptionData.getWrappedContentKey().getAlgorithm())
                            .get();
                } else {
                    if (encryptionData.getWrappedContentKey().getKeyId().equals(this.keyWrapper.getKid())) {
                        contentEncryptionKey = this.keyWrapper
                                .unwrapKeyAsync(encryptionData.getWrappedContentKey().getEncryptedKey(),
                                        encryptionData.getWrappedContentKey().getAlgorithm())
                                .get();
                    } else {
                        throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR, SR.KEY_MISMATCH, null);
                    }
                }

                switch (encryptionData.getEncryptionAgent().getEncryptionAlgorithm()) {
                case AES_CBC_256:

                    Cipher myAes;
                    if (noPadding) {
                        myAes = Cipher.getInstance("AES/CBC/NoPadding");
                    } else {
                        myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
                    }

                    IvParameterSpec ivParameterSpec = new IvParameterSpec(
                            iv != null ? iv : encryptionData.getContentEncryptionIV());
                    SecretKey keySpec = new SecretKeySpec(contentEncryptionKey, 0, contentEncryptionKey.length,
                            "AES");
                    myAes.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);

                    return new CipherOutputStream(userProvidedStream, myAes);

                default:
                    throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
                            SR.INVALID_ENCRYPTION_ALGORITHM, null);
                }
            } else {
                return userProvidedStream;
            }
        } catch (StorageException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR, SR.DECRYPTION_LOGIC_ERROR, ex);
        }
    }

    /**
     * Internal helper method to wrap a user provided stream with the appropriate crypto stream.
     */
    static OutputStream wrapUserStreamWithDecryptStream(CloudBlob blob, OutputStream userProvidedStream,
            BlobRequestOptions options, Map<String, String> metadata, long blobLength, boolean rangeRead,
            Long endOffset, Long userSpecifiedLength, int discardFirst, boolean bufferIV) throws StorageException {
        if (!rangeRead) {
            // The user provided stream should be wrapped in a TruncatingNonCloseableStream in order to 
            // avoid closing the user stream when the crypto stream is closed to flush the final decrypted 
            // block of data.
            OutputStream decryptStream = options.getEncryptionPolicy().decryptBlob(userProvidedStream, metadata,
                    options.requireEncryption(), null, blob.getProperties().getBlobType() == BlobType.PAGE_BLOB);
            return decryptStream;
        } else {
            // Check if end offset lies in the last AES block and send this information over to set the correct padding mode.
            boolean noPadding = blob.getProperties().getBlobType() == BlobType.PAGE_BLOB
                    || endOffset != null && endOffset < blobLength - 16;
            return new BlobDecryptStream(userProvidedStream, metadata, userSpecifiedLength, discardFirst, bufferIV,
                    noPadding, options.getEncryptionPolicy(), options.requireEncryption());
        }
    }

    /**
     * Set up the encryption context required for encrypting blobs.
     * @param metadata
     *          Reference to blob metadata object that is used to set the encryption materials.
     * @param noPadding
     *          Value indicating if the padding mode should be set or not.
     * @return The Cipher to use to decrypt the blob.
     * @throws StorageException
     *             An exception representing any error which occurred during the operation.
     */
    Cipher createAndSetEncryptionContext(Map<String, String> metadata, boolean noPadding) throws StorageException {
        Utility.assertNotNull("metadata", metadata);

        // The Key should be set on the policy for encryption. Otherwise, throw an error.
        if (this.keyWrapper == null) {
            throw new IllegalArgumentException(SR.KEY_MISSING);
        }

        try {
            KeyGenerator keyGen = KeyGenerator.getInstance("AES");
            keyGen.init(256);

            Cipher myAes;
            if (noPadding) {
                myAes = Cipher.getInstance("AES/CBC/NoPadding");
            } else {
                myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
            }

            SecretKey aesKey = keyGen.generateKey();
            myAes.init(Cipher.ENCRYPT_MODE, aesKey);

            BlobEncryptionData encryptionData = new BlobEncryptionData();
            encryptionData.setEncryptionAgent(new EncryptionAgent(
                    Constants.EncryptionConstants.ENCRYPTION_PROTOCOL_V1, EncryptionAlgorithm.AES_CBC_256));

            // Wrap key
            Pair<byte[], String> encryptedKey = this.keyWrapper
                    .wrapKeyAsync(aesKey.getEncoded(), null /* algorithm */).get();
            encryptionData.setWrappedContentKey(new WrappedContentKey(this.keyWrapper.getKid(),
                    encryptedKey.getKey(), encryptedKey.getValue()));

            encryptionData.setContentEncryptionIV(myAes.getIV());

            metadata.put(Constants.EncryptionConstants.BLOB_ENCRYPTION_DATA, encryptionData.serialize());
            return myAes;
        } catch (Exception e) {
            throw StorageException.translateClientException(e);
        }
    }
}