package au.net.causal.projo.prefs.transform;
import org.jasypt.encryption.ByteEncryptor;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import au.net.causal.projo.annotation.Secure;
import au.net.causal.projo.prefs.DataTypeSupport;
import au.net.causal.projo.prefs.PreferenceKeyMetadata;
import au.net.causal.projo.prefs.PreferencesException;
import au.net.causal.projo.prefs.TransformDataTypeSupportChain;
import au.net.causal.projo.prefs.TransformGetChain;
import au.net.causal.projo.prefs.TransformPutChain;
import au.net.causal.projo.prefs.TransformRemoveChain;
import au.net.causal.projo.prefs.TransformResult;
import au.net.causal.projo.prefs.UnsupportedDataTypeException;
import au.net.causal.projo.prefs.security.UserAbortedEnteringPasswordException;
/**
* Encrypts/decrypts any value whose key is annotated with {@literal @}{@link Secure}. Any key marked with this annotation will be processed by this transformer
* regardless of this data type, even if the data type is not supported. This may result in run time errors, but this is better than silently not encrypting
* certain keys.
* <p>
*
* The transformer will attempt to convert the data type to a byte array using other transformers first, then encrypt the data using the supplied encrypter,
* then store this encrypted data, also in byte array form, into the preference store, again using other transformers where needed for data type conversion
* if the store does not support byte arrays natively.
*
* @author prunge
*/
public class EncryptedValueTransformer implements PreferenceTransformer
{
private final ByteEncryptor encrypter;
/**
* Creates an <code>EncryptedValueTransformer</code> that uses the specified encrypter.
*
* @param encrypter the encrypter that performs encryption and decryption of data.
*
* @throws NullPointerException if <code>encrypter</code> is null.
*/
public EncryptedValueTransformer(ByteEncryptor encrypter)
{
if (encrypter == null)
throw new NullPointerException("encrypter == null");
this.encrypter = encrypter;
}
@Override
public <T> TransformResult<T> applyGet(String key, PreferenceKeyMetadata<T> keyMetadata, TransformGetChain chain) throws PreferencesException
{
if (!isSecure(keyMetadata))
return(null);
return(new TransformResult<>(chain.getValueWithRestartedChain(key, keyMetadata.withoutAnnotationType(Secure.class), new DecryptGetChain(chain))));
}
@Override
public <T> boolean applyPut(String key, T value, PreferenceKeyMetadata<T> keyMetadata, TransformPutChain chain) throws PreferencesException
{
if (!isSecure(keyMetadata))
return(false);
//Target is a byte array
chain.putValueWithRestartedChain(key, value, keyMetadata.withoutAnnotationType(Secure.class), new EncryptPutChain(chain));
return(true);
}
@Override
public <T> boolean applyRemove(String key, PreferenceKeyMetadata<T> keyMetadata, TransformRemoveChain chain) throws PreferencesException
{
if (!isSecure(keyMetadata))
return(false);
//Target type will be byte array, always
chain.removeValue(key, keyMetadata.withDataType(byte[].class).withoutAnnotationType(Secure.class));
return(true);
}
@Override
public DataTypeSupport applyDataTypeSupport(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain chain) throws PreferencesException
{
//TODO actually this is affected?
//Since we don't add any supported data types just ignore this
return(null);
}
private boolean isSecure(PreferenceKeyMetadata<?> keyMetadata)
{
return(keyMetadata.isAnnotationPresent(Secure.class));
}
/**
* Encrypts data using the encrypter, throwing appropriate preference exceptions where necessary.
*
* @param data the data to encrypt. Must not be null.
*
* @return the encrypted data.
*
* @throws PreferencesException if the encryption fails or if user-interaction results in the process being cancelled.
*
* @see #decrypt(byte[])
*/
protected byte[] encrypt(byte[] data)
throws PreferencesException
{
try
{
return(encrypter.encrypt(data));
}
catch (EncryptionOperationNotPossibleException e)
{
throw new PreferencesException("Encryption failed: " + e, e);
}
catch (UserAbortedEnteringPasswordException e)
{
throw new PreferencesException("User aborted encryption.", e);
}
}
/**
* Decrypts data using the encrypter, throwing appropriate preference exceptions where necessary.
*
* @param data the data to decrypt. Must not be null.
*
* @return the decrypted data.
*
* @throws PreferencesException if the encryption fails or if user-interaction results in the process being cancelled.
*
* @see #encrypt(byte[])
*/
protected byte[] decrypt(byte[] data)
throws PreferencesException
{
try
{
return(encrypter.decrypt(data));
}
catch (EncryptionOperationNotPossibleException e)
{
throw new PreferencesException("Decryption failed: " + e, e);
}
catch (UserAbortedEnteringPasswordException e)
{
throw new PreferencesException("User aborted decryption.", e);
}
}
/**
* Custom data type support chain that only supports <code>byte[]</code> data types. Used to force the transformers to transform to this data type
* where possible.
*
* @author prunge
*/
private abstract class ChainWithDataTypeSupport implements TransformDataTypeSupportChain
{
@Override
public boolean isDataTypeSupported(PreferenceKeyMetadata<?> keyMetadata) throws PreferencesException
{
if (byte[].class.equals(keyMetadata.getDataType().getRawType()))
return(true);
else
return(false);
}
@Override
public boolean isDataTypeSupportedNatively(PreferenceKeyMetadata<?> keyMetadata) throws PreferencesException
{
return(isDataTypeSupported(keyMetadata));
}
@Override
public boolean isDataTypeSupportedNativelyWithRestartedChain(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain newEndpoint)
throws PreferencesException
{
return(isDataTypeSupported(keyMetadata));
}
@Override
public boolean isDataTypeSupportedWithRestartedChain(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain newEndpoint)
throws PreferencesException
{
return(isDataTypeSupported(keyMetadata));
}
}
/**
* Get chain that performs decryption after reading from the original chain.
*
* @author prunge
*/
private class DecryptGetChain extends ChainWithDataTypeSupport implements TransformGetChain
{
private final TransformGetChain originalChain;
public DecryptGetChain(TransformGetChain originalChain)
{
this.originalChain = originalChain;
}
@Override
public <T> T getValue(String key, PreferenceKeyMetadata<T> keyMetadata)
throws UnsupportedDataTypeException, PreferencesException
{
//Read from original chain, decrypt the data
byte[] encryptedData = originalChain.getValue(key, keyMetadata.withDataType(byte[].class).withoutAnnotationType(Secure.class));
byte[] decryptedData = decrypt(encryptedData);
//Will only ever be byte[]
return((T)decryptedData);
}
@Override
public <T> T getValueWithRestartedChain(String key, PreferenceKeyMetadata<T> keyMetadata, TransformGetChain newEndpoint)
throws UnsupportedDataTypeException, PreferencesException
{
return(getValue(key, keyMetadata));
}
}
/**
* Put chain that performs encryption before storing using the original chain.
* @author prunge
*
*/
private class EncryptPutChain extends ChainWithDataTypeSupport implements TransformPutChain
{
private final TransformPutChain originalChain;
public EncryptPutChain(TransformPutChain originalChain)
{
this.originalChain = originalChain;
}
@Override
public <T> void putValueWithRestartedChain(String key, T value, PreferenceKeyMetadata<T> keyMetadata, TransformPutChain newEndpoint)
throws UnsupportedDataTypeException, PreferencesException
{
putValue(key, value, keyMetadata);
}
@Override
public <T> void putValue(String key, T value, PreferenceKeyMetadata<T> keyMetadata) throws UnsupportedDataTypeException, PreferencesException
{
if (!byte[].class.equals(keyMetadata.getDataType().getRawType()))
throw new UnsupportedDataTypeException(keyMetadata.getDataType());
byte[] bValue = (byte[])value;
bValue = encrypt(bValue);
originalChain.putValue(key, bValue, keyMetadata.withDataType(byte[].class));
}
}
}