/**
* 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.queue;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.microsoft.azure.keyvault.core.IKey;
import com.microsoft.azure.keyvault.extensions.RsaKey;
import com.microsoft.azure.keyvault.extensions.SymmetricKey;
import com.microsoft.azure.storage.DictionaryKeyResolver;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.TestHelper;
import com.microsoft.azure.storage.TestRunners.CloudTests;
import com.microsoft.azure.storage.TestRunners.DevFabricTests;
import com.microsoft.azure.storage.TestRunners.DevStoreTests;
import com.microsoft.azure.storage.core.Base64;
import com.microsoft.azure.storage.core.EncryptionData;
/**
* Queue Tests
*/
@Category({ CloudTests.class, DevFabricTests.class, DevStoreTests.class })
public class CloudQueueEncryptionTests {
private CloudQueue queue;
@Before
public void queueTestMethodSetUp() throws URISyntaxException, StorageException {
this.queue = QueueTestHelper.getRandomQueueReference();
this.queue.createIfNotExists();
}
@After
public void queueTestMethodTearDown() throws StorageException {
this.queue.deleteIfExists();
}
@Test
public void testQueueAddUpdateEncryptedMessage() throws InvalidKeyException, NoSuchAlgorithmException,
NoSuchPaddingException, StorageException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
RsaKey rsaKey = TestHelper.getRSAKey();
// Create the resolver to be used for unwrapping.
DictionaryKeyResolver resolver = new DictionaryKeyResolver();
resolver.add(aesKey);
resolver.add(rsaKey);
doQueueAddUpdateEncryptedMessage(aesKey, resolver);
doQueueAddUpdateEncryptedMessage(rsaKey, resolver);
}
private void doQueueAddUpdateEncryptedMessage(IKey key, DictionaryKeyResolver keyResolver) throws StorageException {
String messageStr = UUID.randomUUID().toString();
CloudQueueMessage message = new CloudQueueMessage(messageStr);
QueueRequestOptions createOptions = new QueueRequestOptions();
createOptions.setEncryptionPolicy(new QueueEncryptionPolicy(key, null));
// add message
this.queue.addMessage(message, 0, 0, createOptions, null);
// Retrieve message
QueueRequestOptions retrieveOptions = new QueueRequestOptions();
retrieveOptions.setEncryptionPolicy(new QueueEncryptionPolicy(null, keyResolver));
CloudQueueMessage retrMessage = this.queue.retrieveMessage(30, retrieveOptions, null);
assertEquals(messageStr, retrMessage.getMessageContentAsString());
// Update message
String updatedMessage = UUID.randomUUID().toString();
retrMessage.setMessageContent(updatedMessage);
this.queue.updateMessage(retrMessage, 0,
EnumSet.of(MessageUpdateFields.CONTENT, MessageUpdateFields.VISIBILITY), createOptions, null);
// Retrieve updated message
retrMessage = this.queue.retrieveMessage(30, retrieveOptions, null);
assertEquals(updatedMessage, retrMessage.getMessageContentAsString());
}
@Test
public void testQueueAddUpdateEncryptedBinaryMessage() throws StorageException, InvalidKeyException,
NoSuchAlgorithmException, NoSuchPaddingException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
byte[] messageBytes = new byte[100];
Random rand = new Random();
rand.nextBytes(messageBytes);
CloudQueueMessage message = new CloudQueueMessage(messageBytes);
QueueRequestOptions options = new QueueRequestOptions();
options.setEncryptionPolicy(new QueueEncryptionPolicy(aesKey, null));
// add message
this.queue.addMessage(message, 0, 0, options, null);
// Retrieve message
CloudQueueMessage retrMessage = this.queue.retrieveMessage(30, options, null);
assertArrayEquals(messageBytes, retrMessage.getMessageContentAsByte());
}
@Test
public void testQueueAddUpdateEncryptedEncodedMessage() throws StorageException, InvalidKeyException,
NoSuchAlgorithmException, NoSuchPaddingException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
byte[] messageBytes = new byte[100];
Random rand = new Random();
rand.nextBytes(messageBytes);
String inputMessage = Base64.encode(messageBytes);
CloudQueueMessage message = new CloudQueueMessage(inputMessage);
this.queue.setShouldEncodeMessage(false);
QueueRequestOptions options = new QueueRequestOptions();
options.setEncryptionPolicy(new QueueEncryptionPolicy(aesKey, null));
// add message
this.queue.addMessage(message, 0, 0, options, null);
// Retrieve message
CloudQueueMessage retrMessage = this.queue.retrieveMessage(30, options, null);
assertEquals(inputMessage, retrMessage.getMessageContentAsString());
}
@Test
public void testQueueAddEncrypted64KMessage() throws StorageException, InvalidKeyException,
NoSuchAlgorithmException, NoSuchPaddingException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
String inputMessage = StringUtils.repeat('a', 64 * 1024);
CloudQueueMessage message = new CloudQueueMessage(inputMessage);
this.queue.setShouldEncodeMessage(false);
QueueRequestOptions options = new QueueRequestOptions();
options.setEncryptionPolicy(new QueueEncryptionPolicy(aesKey, null));
// add message
this.queue.addMessage(message);
// add encrypted Message
try {
this.queue.addMessage(message, 0, 0, options, null);
fail("Adding an encrypted message that exceeds message limits should throw.");
}
catch (IllegalArgumentException e) {
}
}
@Test
public void testQueueMessageValidateEncryption() throws StorageException, JsonProcessingException, IOException,
InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException,
NoSuchAlgorithmException, NoSuchPaddingException, InterruptedException, ExecutionException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
byte[] messageBytes = new byte[100];
Random rand = new Random();
rand.nextBytes(messageBytes);
String inputMessage = Base64.encode(messageBytes);
CloudQueueMessage message = new CloudQueueMessage(inputMessage);
this.queue.setShouldEncodeMessage(false);
QueueRequestOptions options = new QueueRequestOptions();
options.setEncryptionPolicy(new QueueEncryptionPolicy(aesKey, null));
// add message
this.queue.addMessage(message, 0, 0, options, null);
// Retrieve message without decrypting
CloudQueueMessage retrMessage = this.queue.retrieveMessage();
// Decrypt locally
CloudQueueMessage decryptedMessage;
CloudQueueEncryptedMessage encryptedMessage = CloudQueueEncryptedMessage.deserialize(retrMessage
.getMessageContentAsString());
EncryptionData encryptionData = encryptedMessage.getEncryptionData();
byte[] contentEncryptionKey = aesKey.unwrapKeyAsync(encryptionData.getWrappedContentKey().getEncryptedKey(),
encryptionData.getWrappedContentKey().getAlgorithm()).get();
SecretKey keySpec = new SecretKeySpec(contentEncryptionKey, 0, contentEncryptionKey.length,
"AES");
Cipher myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
myAes.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(encryptionData.getContentEncryptionIV()));
byte[] src = Base64.decode(encryptedMessage.getEncryptedMessageContents());
decryptedMessage = new CloudQueueMessage(myAes.doFinal(src, 0, src.length));
assertArrayEquals(message.getMessageContentAsByte(), decryptedMessage.getMessageContentAsByte());
}
@Test
public void testQueueMessageEncryptionWithStrictMode() throws StorageException, InvalidKeyException,
NoSuchAlgorithmException, NoSuchPaddingException {
// Create the Key to be used for wrapping.
SymmetricKey aesKey = TestHelper.getSymmetricKey();
// Create the resolver to be used for unwrapping.
DictionaryKeyResolver resolver = new DictionaryKeyResolver();
resolver.add(aesKey);
String messageStr = UUID.randomUUID().toString();
CloudQueueMessage message = new CloudQueueMessage(messageStr);
// Add message with policy.
QueueRequestOptions createOptions = new QueueRequestOptions();
createOptions.setEncryptionPolicy(new QueueEncryptionPolicy(aesKey, null));
createOptions.setRequireEncryption(true);
this.queue.addMessage(message, 0, 0, createOptions, null);
// Set policy to null and add message while RequireEncryption flag is still set to true. This should throw.
createOptions.setEncryptionPolicy(null);
try {
this.queue.addMessage(message, 0, 0, createOptions, null);
fail("Not specifying a policy when RequireEnryption is set to true should throw.");
}
catch (IllegalArgumentException ex) {
}
// Retrieve message
QueueRequestOptions retrieveOptions = new QueueRequestOptions();
retrieveOptions.setEncryptionPolicy(new QueueEncryptionPolicy(null, resolver));
retrieveOptions.setRequireEncryption(true);
CloudQueueMessage retrMessage = queue.retrieveMessage(30, retrieveOptions, null);
// Update message with plain text.
String updatedMessage = UUID.randomUUID().toString();
retrMessage.setMessageContent(updatedMessage);
this.queue.updateMessage(retrMessage, 0,
EnumSet.of(MessageUpdateFields.CONTENT, MessageUpdateFields.VISIBILITY), null, null);
// Retrieve updated message with RequireEncryption flag but no metadata on the service. This should throw.
try {
this.queue.retrieveMessage(30, retrieveOptions, null);
fail("Retrieving with RequireEncryption set to true and no metadata on the service should fail.");
}
catch (StorageException ex) {
}
// Set RequireEncryption to false and retrieve.
retrieveOptions.setRequireEncryption(false);
queue.retrieveMessage(30, retrieveOptions, null);
}
}