com.microsoft.azure.storage.table.TableEncryptionPolicy Maven / Gradle / Ivy
Show all versions of azure-storage Show documentation
/**
* 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.table;
import java.security.Key;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.crypto.Cipher;
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.EncryptionData;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;
import com.microsoft.azure.storage.core.WrappedContentKey;
import com.microsoft.azure.storage.table.TableRequestOptions.EncryptionResolver;
/**
* Represents a table encryption policy that is used to perform envelope encryption/decryption of Azure table entities.
*/
public class TableEncryptionPolicy {
/**
* An object of type {@link IKey} that is used to wrap/unwrap the content key during encryption.
*/
public IKey keyWrapper;
/**
* The {@link IKeyResolver} used to select the correct key for decrypting existing table entities.
*/
public IKeyResolver keyResolver;
/**
* Initializes a new instance of the {@link TableEncryptionPolicy} class with the specified key and resolver.
*
* 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 table entities.
*/
public TableEncryptionPolicy(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 table entities.
*
* @return A resolver that returns an {@link SymmetricKey} 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 table entities.
*
* @param keyResolver
* A resolver that returns an {@link IKey} given a keyId.
*/
public void setKeyResolver(IKeyResolver keyResolver) {
this.keyResolver = keyResolver;
}
/**
* Return an encrypted entity. This method is used for encrypting entity properties.
*/
Map encryptEntity(Map properties, String partitionKey,
String rowKey, EncryptionResolver encryptionResolver) throws StorageException {
Utility.assertNotNull("properties", properties);
// The Key should be set on the policy for encryption. Otherwise, throw an error.
if (this.keyWrapper == null) {
throw new IllegalArgumentException(SR.KEY_MISSING);
}
EncryptionData encryptionData = new EncryptionData();
encryptionData.setEncryptionAgent(new EncryptionAgent(Constants.EncryptionConstants.ENCRYPTION_PROTOCOL_V1,
EncryptionAlgorithm.AES_CBC_256));
try {
Map encryptedProperties = new HashMap();
HashSet encryptionPropertyDetailsSet = new HashSet();
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
Cipher myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKey aesKey = keyGen.generateKey();
myAes.init(Cipher.ENCRYPT_MODE, aesKey);
// Wrap key
Pair encryptedKey = this.keyWrapper
.wrapKeyAsync(aesKey.getEncoded(), null /* algorithm */).get();
encryptionData.setWrappedContentKey(new WrappedContentKey(this.keyWrapper.getKid(), encryptedKey.getKey(),
encryptedKey.getValue()));
encryptionData.setContentEncryptionIV(myAes.getIV());
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (Map.Entry kvp : properties.entrySet()) {
if (encryptionResolver != null
&& encryptionResolver.encryptionResolver(partitionKey, rowKey, kvp.getKey())) {
// Throw if users try to encrypt null properties. This could happen in the DynamicTableEntity case
// where a user adds a new property as follows - ent.Properties.Add("foo2", null);
if (kvp.getValue() == null) {
throw new IllegalArgumentException(SR.ENCRYPTING_NULL_PROPERTIES_NOT_ALLOWED);
}
kvp.getValue().setIsEncrypted(true);
}
// IsEncrypted is set to true when either the EncryptPropertyAttribute is set on a property or when it is
// specified in the encryption resolver or both.
if (kvp.getValue() != null && kvp.getValue().isEncrypted()) {
// Throw if users try to encrypt non-string properties.
if (kvp.getValue().getEdmType() != EdmType.STRING) {
throw new IllegalArgumentException(String.format(SR.UNSUPPORTED_PROPERTY_TYPE_FOR_ENCRYPTION,
kvp.getValue().getEdmType()));
}
byte[] columnIVFull = digest.digest(Utility.binaryAppend(encryptionData.getContentEncryptionIV(),
(partitionKey + rowKey + kvp.getKey()).getBytes(Constants.UTF8_CHARSET)));
byte[] columnIV = new byte[16];
System.arraycopy(columnIVFull, 0, columnIV, 0, 16);
myAes.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(columnIV));
// Throw if users try to encrypt null properties. This could happen in the DynamicTableEntity or POCO
// case when the property value is null.
if (kvp.getValue() == null) {
throw new IllegalArgumentException(SR.ENCRYPTING_NULL_PROPERTIES_NOT_ALLOWED);
}
byte[] src = kvp.getValue().getValueAsString().getBytes(Constants.UTF8_CHARSET);
byte[] dest = myAes.doFinal(src, 0, src.length);
// Store the encrypted properties as binary values on the service instead of base 64 encoded strings because strings are stored as a sequence of
// WCHARs thereby further reducing the allowed size by half. During retrieve, it is handled by the response parsers correctly
// even when the service does not return the type for JSON no-metadata.
encryptedProperties.put(kvp.getKey(), new EntityProperty(dest));
encryptionPropertyDetailsSet.add(kvp.getKey());
}
else {
encryptedProperties.put(kvp.getKey(), kvp.getValue());
}
// Encrypt the property details set and add it to entity properties.
byte[] metadataIVFull = digest.digest(Utility.binaryAppend(encryptionData.getContentEncryptionIV(),
(partitionKey + rowKey + Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS)
.getBytes(Constants.UTF8_CHARSET)));
byte[] metadataIV = new byte[16];
System.arraycopy(metadataIVFull, 0, metadataIV, 0, 16);
myAes.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(metadataIV));
byte[] src = Arrays.toString(encryptionPropertyDetailsSet.toArray()).getBytes(Constants.UTF8_CHARSET);
byte[] dest = myAes.doFinal(src, 0, src.length);
encryptedProperties.put(Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS,
new EntityProperty(dest));
}
encryptedProperties.put(Constants.EncryptionConstants.TABLE_ENCRYPTION_KEY_DETAILS, new EntityProperty(
encryptionData.serialize()));
return encryptedProperties;
}
catch (Exception e) {
throw StorageException.translateClientException(e);
}
}
Key decryptMetadataAndReturnCEK(String partitionKey, String rowKey, EntityProperty encryptionKeyProperty,
EntityProperty propertyDetailsProperty, EncryptionData encryptionData) throws StorageException
{
// 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);
}
try {
// Copy the values into the passed in encryption data object so they can be accessed outside of this context
encryptionData.copyValues(EncryptionData.deserialize(encryptionKeyProperty.getValueAsString()));
Utility.assertNotNull("contentEncryptionIV", encryptionData.getContentEncryptionIV());
Utility.assertNotNull("encryptedKey", encryptionData.getWrappedContentKey().getEncryptedKey());
// Throw if the encryption protocol on the entity 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);
}
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);
}
}
Cipher myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] metadataIVFull = sha256.digest(Utility.binaryAppend(encryptionData.getContentEncryptionIV(),
(partitionKey + rowKey + Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS)
.getBytes(Constants.UTF8_CHARSET)));
byte[] metadataIV = new byte[16];
System.arraycopy(metadataIVFull, 0, metadataIV, 0, 16);
IvParameterSpec ivParameterSpec = new IvParameterSpec(metadataIV);
SecretKey keySpec = new SecretKeySpec(contentEncryptionKey, 0, contentEncryptionKey.length,
"AES");
myAes.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
byte[] src = propertyDetailsProperty.getValueAsByteArray();
propertyDetailsProperty.setValue(myAes.doFinal(src, 0, src.length));
return keySpec;
}
catch (StorageException ex) {
throw ex;
}
catch (Exception ex) {
throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR, SR.DECRYPTION_LOGIC_ERROR, ex);
}
}
/**
* Return a decrypted entity. This method is used for decrypting entity properties.
*/
HashMap decryptEntity(HashMap properties,
HashSet encryptedPropertyDetailsSet, String partitionKey, String rowKey, Key contentEncryptionKey,
EncryptionData encryptionData) throws StorageException {
HashMap decryptedProperties = new HashMap();
try {
switch (encryptionData.getEncryptionAgent().getEncryptionAlgorithm()) {
case AES_CBC_256:
Cipher myAes = Cipher.getInstance("AES/CBC/PKCS5Padding");
for (Map.Entry kvp : properties.entrySet()) {
if (kvp.getKey() == Constants.EncryptionConstants.TABLE_ENCRYPTION_KEY_DETAILS
|| kvp.getKey() == Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS) {
// Do nothing. Do not add to the result properties.
}
else if (encryptedPropertyDetailsSet.contains(kvp.getKey())) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] columnIVFull = digest.digest(Utility.binaryAppend(
encryptionData.getContentEncryptionIV(),
(partitionKey + rowKey + kvp.getKey()).getBytes(Constants.UTF8_CHARSET)));
byte[] columnIV = new byte[16];
System.arraycopy(columnIVFull, 0, columnIV, 0, 16);
myAes.init(Cipher.DECRYPT_MODE, contentEncryptionKey, new IvParameterSpec(columnIV));
byte[] src = kvp.getValue().getValueAsByteArray();
byte[] dest = myAes.doFinal(src, 0, src.length);
String destString = new String(dest, Constants.UTF8_CHARSET);
decryptedProperties.put(kvp.getKey(), new EntityProperty(destString));
}
else {
decryptedProperties.put(kvp.getKey(), kvp.getValue());
}
}
return decryptedProperties;
default:
throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
SR.INVALID_ENCRYPTION_ALGORITHM, null);
}
}
catch (StorageException ex) {
throw ex;
}
catch (Exception ex) {
throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR, SR.DECRYPTION_LOGIC_ERROR, ex);
}
}
}