in)
@@ -148,14 +147,17 @@ public class NodePropertyHelper
// Get or spoof the property datatype
QName propertyTypeQName;
+ boolean isEncrypted;
if (propertyDef == null) // property not recognised
{
// allow it for now - persisting excess properties can be useful sometimes
propertyTypeQName = DataTypeDefinition.ANY;
+ isEncrypted = false;
}
else
{
propertyTypeQName = propertyDef.getDataType().getName();
+ isEncrypted = propertyDef.isEncrypted();
}
// A property may appear to be multi-valued if the model definition is loose and
@@ -266,19 +268,8 @@ public class NodePropertyHelper
// Get the Locale ID for the text
Long mlTextLocaleId = localeDAO.getOrCreateLocalePair(mlTextLocale).getFirst();
// This is persisted against the current locale, but as a d:text instance
- Serializable v = null;
- try
- {
- v = propertyDef.isEncrypted() ? encrypt(mlTextStr) : mlTextStr;
- }
- catch (UnsupportedEncodingException e)
- {
- // TODO check that throwing the exception preserves the original logic
- throw new TypeConversionException(
- "The property value could not be decoded as a UTF-8 string " + value.getClass(),
- e);
- }
- NodePropertyValue npValue = new NodePropertyValue(DataTypeDefinition.TEXT, v, propertyDef.isEncrypted());
+ // This is persisted against the current locale, but as a d:text instance
+ NodePropertyValue npValue = new NodePropertyValue(DataTypeDefinition.TEXT, mlTextStr);
NodePropertyKey npKey = new NodePropertyKey();
npKey.setListIndex(collectionIndex);
npKey.setQnameId(propertyQNameId);
@@ -289,29 +280,6 @@ public class NodePropertyHelper
}
else
{
- if(!propertyTypeQName.equals(DataTypeDefinition.ANY) && propertyDef.isEncrypted())
- {
- if(propertyTypeQName.equals(DataTypeDefinition.TEXT))
- {
- try
- {
- // TODO check type of value
- value = propertyDef.isEncrypted() ? encrypt((String)value) : value;
- }
- catch (UnsupportedEncodingException e)
- {
- // TODO check that throwing the exception preserves the original logic
- throw new TypeConversionException(
- "The property value could not be decoded as a UTF-8 string " + value.getClass(),
- e);
- }
- }
- else
- {
- logger.warn("Encryption is not supported for type " + propertyTypeQName + ", encryption will not be performed");
- }
- }
-
NodePropertyValue npValue = makeNodePropertyValue(propertyDef, value);
NodePropertyKey npKey = new NodePropertyKey();
npKey.setListIndex(collectionIndex);
@@ -323,18 +291,6 @@ public class NodePropertyHelper
}
}
- protected byte[] encrypt(String input) throws UnsupportedEncodingException
- {
- byte[] bytes = encryptionEngine.encryptString(input);
- return bytes;
- }
-
- protected String decrypt(byte[] input) throws UnsupportedEncodingException
- {
- String s = encryptionEngine.decryptAsString(input);
- return s;
- }
-
/**
* Helper method to convert the Serializable
value into a full, persistable {@link NodePropertyValue}.
*
@@ -364,8 +320,7 @@ public class NodePropertyHelper
try
{
NodePropertyValue propertyValue = null;
- boolean isEncrypted = propertyDef==null ? false : propertyDef.isEncrypted();
- propertyValue = new NodePropertyValue(propertyTypeQName, value, isEncrypted);
+ propertyValue = new NodePropertyValue(propertyTypeQName, value);
// done
return propertyValue;
@@ -697,17 +652,17 @@ public class NodePropertyHelper
}
// get property attributes
final QName propertyTypeQName;
- boolean encrypted;
+ boolean isEncrypted;
if (propertyDef == null)
{
// allow this for now
propertyTypeQName = DataTypeDefinition.ANY;
- encrypted = false;
+ isEncrypted = false;
}
else
{
propertyTypeQName = propertyDef.getDataType().getName();
- encrypted = propertyDef.isEncrypted();
+ isEncrypted = propertyDef.isEncrypted();
}
try
{
@@ -727,24 +682,6 @@ public class NodePropertyHelper
ContentData contentData = contentDataDAO.getContentData(contentDataId).getSecond();
value = new ContentDataWithId(contentData, contentDataId);
}
- else if (encrypted)
- {
- if (propertyTypeQName.equals(DataTypeDefinition.TEXT) || propertyTypeQName.equals(DataTypeDefinition.MLTEXT))
- {
- try
- {
- value = decrypt((byte[])value);
- }
- catch (UnsupportedEncodingException e)
- {
- throw new AlfrescoRuntimeException("Unexpected exception during decryption", e);
- }
- }
- else
- {
- throw new AlfrescoRuntimeException("Encryption is not supported for " + propertyDef.getDataType().getName() + " types");
- }
- }
// done
return value;
}
diff --git a/source/java/org/alfresco/repo/domain/node/NodePropertyHelperTest.java b/source/java/org/alfresco/repo/domain/node/NodePropertyHelperTest.java
index cf5c5a41c6..6f58c9eb9d 100644
--- a/source/java/org/alfresco/repo/domain/node/NodePropertyHelperTest.java
+++ b/source/java/org/alfresco/repo/domain/node/NodePropertyHelperTest.java
@@ -32,7 +32,7 @@ import org.alfresco.model.ContentModel;
import org.alfresco.repo.domain.contentdata.ContentDataDAO;
import org.alfresco.repo.domain.locale.LocaleDAO;
import org.alfresco.repo.domain.qname.QNameDAO;
-import org.alfresco.repo.security.encryption.EncryptionEngine;
+import org.alfresco.repo.security.encryption.Encryptor;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.version.VersionModel;
@@ -87,9 +87,9 @@ public class NodePropertyHelperTest extends TestCase
QNameDAO qnameDAO = (QNameDAO) ctx.getBean("qnameDAO");
LocaleDAO localeDAO = (LocaleDAO) ctx.getBean("localeDAO");
ContentDataDAO contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO");
- EncryptionEngine encryptionEngine = (EncryptionEngine) ctx.getBean("encryptionEngine");
+ Encryptor encryptor = (Encryptor) ctx.getBean("encryptor");
- helper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO, encryptionEngine);
+ helper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO, encryptor);
transactionService = serviceRegistry.getTransactionService();
txnHelper = transactionService.getRetryingTransactionHelper();
txnHelper.setMinRetryWaitMs(10);
diff --git a/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java b/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java
index 3d8341ebc9..5d8fbd50ae 100644
--- a/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java
+++ b/source/java/org/alfresco/repo/domain/node/NodePropertyValue.java
@@ -23,7 +23,6 @@ import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
-import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -31,11 +30,11 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import javax.crypto.SealedObject;
+
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.domain.schema.SchemaBootstrap;
-import org.alfresco.repo.security.encryption.EncryptionEngine;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
-import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
@@ -527,6 +526,37 @@ public class NodePropertyValue implements Cloneable, Serializable
}
}
},
+ SEALED_OBJECT
+ {
+ @Override
+ public Integer getOrdinalNumber()
+ {
+ return Integer.valueOf(22);
+ }
+
+ @Override
+ protected ValueType getPersistedType(Serializable value)
+ {
+ return ValueType.SERIALIZABLE;
+ }
+
+ @Override
+ Serializable convert(Serializable value)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+ else if (value instanceof SealedObject)
+ {
+ return value;
+ }
+ else
+ {
+ throw new IllegalArgumentException("SealedObject value not supported: " + value);
+ }
+ }
+ },
;
/**
@@ -637,6 +667,10 @@ public class NodePropertyValue implements Cloneable, Serializable
{
return ValueType.CONTENT;
}
+ else if (value instanceof SealedObject)
+ {
+ return ValueType.SEALED_OBJECT;
+ }
else
{
// type is not recognised as belonging to any particular slot
@@ -732,8 +766,6 @@ public class NodePropertyValue implements Cloneable, Serializable
private Double doubleValue;
private String stringValue;
private Serializable serializableValue;
-
- private boolean encrypted;
/**
* default constructor
@@ -748,16 +780,13 @@ public class NodePropertyValue implements Cloneable, Serializable
* @param typeQName the dictionary-defined property type to store the property as
* @param value the value to store. This will be converted into a format compatible
* with the type given
- * @param isEncrypted true if value should be encrypted when persisted
*
* @throws java.lang.UnsupportedOperationException if the value cannot be converted to the type given
*/
- public NodePropertyValue(QName typeQName, Serializable value, boolean encrypted)
+ public NodePropertyValue(QName typeQName, Serializable value)
{
ParameterCheck.mandatory("typeQName", typeQName);
- this.encrypted = encrypted;
-
if (value == null)
{
this.actualType = NodePropertyValue.getActualType(value);
@@ -765,38 +794,17 @@ public class NodePropertyValue implements Cloneable, Serializable
}
else
{
- ValueType persistedValueType = null;
-
- if(encrypted)
- {
- // this constructor doesn't appear to get called for type DataTypeDefinition.MLTEXT because MLTEXT is
- // split out into strings in NodePropertyHelper
- if(typeQName.equals(DataTypeDefinition.TEXT))
- {
- this.actualType = ValueType.STRING;
- persistedValueType = ValueType.SERIALIZABLE;
- }
- else
- {
- throw new AlfrescoRuntimeException("Can encrypt only TEXT and MLTEXT types, this type is " + typeQName);
- }
- }
- else
- {
- // Convert the value to the type required. This ensures that any type conversion issues
- // are caught early and prevent the scenario where the data in the DB cannot be given
- // back out because it is unconvertable.
- ValueType valueType = makeValueType(typeQName);
- value = valueType.convert(value);
-
- this.actualType = NodePropertyValue.getActualType(value);
-
- // get the persisted type
- persistedValueType = this.actualType.getPersistedType(value);
- // convert to the persistent type
- value = persistedValueType.convert(value);
- }
-
+ // Convert the value to the type required. This ensures that any type conversion issues
+ // are caught early and prevent the scenario where the data in the DB cannot be given
+ // back out because it is unconvertable.
+ ValueType valueType = makeValueType(typeQName);
+ value = valueType.convert(value);
+
+ this.actualType = NodePropertyValue.getActualType(value);
+ // get the persisted type
+ ValueType persistedValueType = this.actualType.getPersistedType(value);
+ // convert to the persistent type
+ value = persistedValueType.convert(value);
setPersistedValue(persistedValueType, value);
}
}
@@ -1070,9 +1078,9 @@ public class NodePropertyValue implements Cloneable, Serializable
// have been converted on the way in.
ret = (Serializable) persistedValue;
}
- else if(encrypted)
+ else if(persistedValue instanceof SealedObject)
{
- ret = persistedValue;
+ ret = (Serializable) persistedValue;
}
else
{
diff --git a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java
index e677bcdd55..c4cb434574 100644
--- a/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java
+++ b/source/java/org/alfresco/repo/node/getchildren/GetChildrenCannedQueryFactory.java
@@ -29,8 +29,8 @@ import org.alfresco.query.CannedQuery;
import org.alfresco.query.CannedQueryPageDetails;
import org.alfresco.query.CannedQueryParameters;
import org.alfresco.query.CannedQuerySortDetails;
-import org.alfresco.query.PagingRequest;
import org.alfresco.query.CannedQuerySortDetails.SortOrder;
+import org.alfresco.query.PagingRequest;
import org.alfresco.repo.domain.contentdata.ContentDataDAO;
import org.alfresco.repo.domain.locale.LocaleDAO;
import org.alfresco.repo.domain.node.NodeDAO;
@@ -38,7 +38,7 @@ import org.alfresco.repo.domain.node.NodePropertyHelper;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.domain.query.CannedQueryDAO;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.security.encryption.EncryptionEngine;
+import org.alfresco.repo.security.encryption.Encryptor;
import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
@@ -64,7 +64,7 @@ public class GetChildrenCannedQueryFactory extends AbstractCannedQueryFactory getCannedQuery(CannedQueryParameters parameters)
{
- NodePropertyHelper nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO, encryptionEngine);
+ NodePropertyHelper nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO, encryptor);
Method method = null;
for (Method m : methodService.getClass().getMethods())
diff --git a/source/java/org/alfresco/repo/security/authentication/BasicPasswordGenerator.java b/source/java/org/alfresco/repo/security/authentication/BasicPasswordGenerator.java
index 44a147e7bc..fe406350f3 100644
--- a/source/java/org/alfresco/repo/security/authentication/BasicPasswordGenerator.java
+++ b/source/java/org/alfresco/repo/security/authentication/BasicPasswordGenerator.java
@@ -49,4 +49,12 @@ public class BasicPasswordGenerator implements PasswordGenerator
{
return RandomStringUtils.randomAlphanumeric(passwordLength);
}
+
+ public static void main(String ... args)
+ {
+ BasicPasswordGenerator pwdGen = new BasicPasswordGenerator();
+ pwdGen.setPasswordLength(10);
+
+ System.out.println("A password: " + pwdGen.generatePassword());
+ }
}
diff --git a/source/java/org/alfresco/repo/security/encryption/AESEncryptionEngine.java b/source/java/org/alfresco/repo/security/encryption/AESEncryptionEngine.java
deleted file mode 100644
index 2b6640af04..0000000000
--- a/source/java/org/alfresco/repo/security/encryption/AESEncryptionEngine.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package org.alfresco.repo.security.encryption;
-
-import java.io.UnsupportedEncodingException;
-import java.security.Key;
-import java.security.Security;
-
-import javax.crypto.Cipher;
-
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-
-public class AESEncryptionEngine implements EncryptionEngine
-{
- private static String ALGORITHM = "AES/ECB/PKCS5Padding";
- private static final Log logger = LogFactory.getLog(AESEncryptionEngine.class);
-
- private KeyProvider keyProvider;
- //private Key key;
-
- private ThreadLocal cipher;
-
- public AESEncryptionEngine()
- {
- }
-
- public void setKeyProvider(KeyProvider keyProvider)
- {
- this.keyProvider = keyProvider;
- }
-
- public void init()
- {
- this.cipher = new ThreadLocal();
-// key = keyProvider.getKey();
-// if(key == null)
-// {
-// throw new AlfrescoRuntimeException("Secret key is null.");
-// }
- }
-
- protected byte[] process(int cipherMode, byte[] input)
- {
- Cipher cipher = this.cipher.get();
-
- if(cipher == null)
- {
- try
- {
- cipher = Cipher.getInstance(ALGORITHM);
- }
- catch(Exception e)
- {
- Security.addProvider(new BouncyCastleProvider());
- try
- {
- cipher = Cipher.getInstance(ALGORITHM);
- }
- catch(Exception e1)
- {
- throw new AlfrescoRuntimeException("Unable to initialise encryption engine", e1);
- }
- }
-
- if(cipher == null)
- {
- throw new AlfrescoRuntimeException("Unable to initialise encryption engine");
- }
-
- this.cipher.set(cipher);
-
- logger.debug("Initialised thread local cipher");
- }
-
- try
- {
- cipher.init(cipherMode, keyProvider.getKey());
-
- // do the encryption/decryption in one go
- return cipher.doFinal(input);
- }
- catch(Exception e)
- {
- throw new AlfrescoRuntimeException("Unexpected exception during encryption/decryption", e);
- }
- }
-
- public byte[] encrypt(byte[] input)
- {
- return process(Cipher.ENCRYPT_MODE, input);
- }
-
- public byte[] decrypt(byte[] input)
- {
- return process(Cipher.DECRYPT_MODE, input);
- }
-
- public byte[] encryptString(String input) throws UnsupportedEncodingException
- {
- byte[] in = input.getBytes("UTF-8");
- return encrypt(in);
- }
-
- public String decryptAsString(byte[] input) throws UnsupportedEncodingException
- {
- return new String(decrypt(input), "UTF-8").trim();
- }
-}
diff --git a/source/java/org/alfresco/repo/security/encryption/AbstractEncryptor.java b/source/java/org/alfresco/repo/security/encryption/AbstractEncryptor.java
new file mode 100644
index 0000000000..172bf9d56c
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/encryption/AbstractEncryptor.java
@@ -0,0 +1,189 @@
+package org.alfresco.repo.security.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.SealedObject;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.Pair;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Basic support for encryption engines.
+ *
+ * @since 4.0
+ */
+public abstract class AbstractEncryptor implements Encryptor
+{
+ private static final Log logger = LogFactory.getLog(AbstractEncryptor.class);
+
+ private KeyProvider keyProvider;
+
+ /**
+ * Constructs with defaults
+ */
+ protected AbstractEncryptor()
+ {
+ }
+
+ /**
+ * @param keyProvider provides encryption keys based on aliases
+ */
+ public void setKeyProvider(KeyProvider keyProvider)
+ {
+ this.keyProvider = keyProvider;
+ }
+
+ public void init()
+ {
+ PropertyCheck.mandatory(this, "keyProvider", keyProvider);
+ }
+
+ @Override
+ public Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode)
+ {
+ // Get the encryption key
+ Key key = keyProvider.getKey(keyAlias);
+ if (key == null)
+ {
+ // No encryption possible
+ return null;
+ }
+ try
+ {
+ Cipher cipher = getCipher(key, params, mode);
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Cipher constructed: alias=" + keyAlias + "; mode=" + mode + ": " + cipher);
+ }
+ return cipher;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to construct cipher: alias=" + keyAlias + "; mode=" + mode,
+ e);
+ }
+ }
+
+ /**
+ * Factory method to be written by implementations to construct and initialize
+ * physical ciphering objects.
+ *
+ * @param keyAlias the key alias
+ * @param params algorithm-specific parameters
+ * @param mode the cipher mode
+ * @return
+ */
+ protected abstract Cipher getCipher(Key key, AlgorithmParameters params, int mode) throws Exception;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE);
+ if (cipher == null)
+ {
+ return new Pair(input, null);
+ }
+ try
+ {
+ byte[] output = cipher.doFinal(input);
+ params = cipher.getParameters();
+ return new Pair(output, params);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+ try
+ {
+ return cipher.doFinal(input);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Serializes and {@link #encrypt(byte[]) encrypts} the input data.
+ */
+ @Override
+ public Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input)
+ {
+ try
+ {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(input);
+ byte[] unencrypted = bos.toByteArray();
+ return encrypt(keyAlias, params, unencrypted);
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to serialize or encrypt object", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * {@link #decrypt(byte[]) Decrypts} and deserializes the input data
+ */
+ @Override
+ public Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ try
+ {
+ byte[] unencrypted = decrypt(keyAlias, params, input);
+ ByteArrayInputStream bis = new ByteArrayInputStream(unencrypted);
+ ObjectInputStream ois = new ObjectInputStream(bis);
+ Object obj = ois.readObject();
+ return obj;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to deserialize or decrypt object", e);
+ }
+ }
+
+ @Override
+ public SealedObject sealObject(String keyAlias, AlgorithmParameters params, Serializable input)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Serializable unsealObject(String keyAlias, SealedObject input)
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/encryption/AbstractKeyProvider.java b/source/java/org/alfresco/repo/security/encryption/AbstractKeyProvider.java
index 28a48a5840..4ab5f7e640 100644
--- a/source/java/org/alfresco/repo/security/encryption/AbstractKeyProvider.java
+++ b/source/java/org/alfresco/repo/security/encryption/AbstractKeyProvider.java
@@ -1,101 +1,21 @@
package org.alfresco.repo.security.encryption;
-import java.security.InvalidParameterException;
import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.Security;
-import javax.crypto.KeyGenerator;
+import org.alfresco.util.ParameterCheck;
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.alfresco.repo.security.authentication.PasswordGenerator;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-
-public abstract class AbstractKeyProvider /*extends AbstractLifecycleBean*/ implements KeyProvider
+/**
+ * Basic support for key providers
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractKeyProvider implements KeyProvider
{
- private static final Log logger = LogFactory.getLog(KeyProvider.class);
-
- private static int KEY_SIZE = 256; // this requires unlimited strength policy files
- private static int DEFAULT_KEY_SIZE = 128; // default key size should work if KEY_SIZE doesn't
- private static String KEY_ALGORITHM = "AES";
-
- protected PasswordGenerator passwordGenerator;
-
- private Key key;
-
- public void setKey(Key key)
+ @Override
+ public Key getKey(AlfrescoKeyAlias keyAlias)
{
- this.key = key;
- }
-
-
- public PasswordGenerator getPasswordGenerator()
- {
- return passwordGenerator;
- }
-
- public void setPasswordGenerator(PasswordGenerator passwordGenerator)
- {
- this.passwordGenerator = passwordGenerator;
- }
-
- public Key getKey()
- {
- return key;
- }
-
- protected KeyGenerator getKeyGenerator()
- {
- KeyGenerator keyGenerator = null;
-
- try
- {
- keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- }
- catch(NoSuchAlgorithmException e)
- {
- Security.addProvider(new BouncyCastleProvider());
- try
- {
- keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- }
- catch(NoSuchAlgorithmException e1)
- {
- throw new AlfrescoRuntimeException("Unable to initialise encryption engine, no key generator is available", e1);
- }
- }
-
- if(keyGenerator == null)
- {
- throw new AlfrescoRuntimeException("Unable to initialise encryption engine, no key generator is available");
- }
-
- try
- {
- keyGenerator.init(KEY_SIZE);
- }
- catch(InvalidParameterException e)
- {
- logger.warn(KEY_SIZE + " bits key size is not supported, trying " + DEFAULT_KEY_SIZE + " bits");
- try
- {
- // try a smaller key size
- keyGenerator.init(DEFAULT_KEY_SIZE);
- }
- catch(InvalidParameterException e1)
- {
- throw new AlfrescoRuntimeException("Unable to initialise encryption engine, no key generator is available", e1);
- }
- }
-
- return keyGenerator;
- }
-
- protected Key generateSecretKey()
- {
- KeyGenerator keyGenerator = getKeyGenerator();
- return keyGenerator.generateKey();
+ ParameterCheck.mandatory("keyAlias", keyAlias);
+ return getKey(keyAlias.name());
}
}
diff --git a/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngine.java b/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngine.java
deleted file mode 100644
index b725e90aae..0000000000
--- a/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngine.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package org.alfresco.repo.security.encryption;
-
-import java.io.UnsupportedEncodingException;
-import java.security.Security;
-
-import javax.crypto.Cipher;
-
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bouncycastle.crypto.BufferedBlockCipher;
-import org.bouncycastle.crypto.CipherParameters;
-import org.bouncycastle.crypto.InvalidCipherTextException;
-import org.bouncycastle.crypto.engines.AESEngine;
-import org.bouncycastle.crypto.paddings.BlockCipherPadding;
-import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
-import org.bouncycastle.crypto.paddings.ZeroBytePadding;
-import org.bouncycastle.crypto.params.KeyParameter;
-
-public class DefaultEncryptionEngine implements EncryptionEngine
-{
- private static final Log logger = LogFactory.getLog(EncryptionEngine.class);
-
- //private String encryptionProvider;
- private KeyProvider keyProvider;
-
- private BufferedBlockCipher cipher;
- private AESEngine engine;
-
- //private Cipher cipher;
- //private byte[] key;
-
- public DefaultEncryptionEngine(/*byte[] key*/)
- {
- // TODO check that this hasn't already been done
- Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
-//
-// this.key = key;
- }
-
- public void setKeyProvider(KeyProvider keyProvider)
- {
- this.keyProvider = keyProvider;
- }
-
-// public void setEncryptionProvider(String encryptionProvider)
-// {
-// this.encryptionProvider = encryptionProvider;
-// }
-
- public void init()
- {
- //cipher = Cipher.getInstance("AES");
- this.engine = new AESEngine();
-
- /*
- * Paddings available (http://www.bouncycastle.org/docs/docs1.6/org/bouncycastle/crypto/paddings/BlockCipherPadding.html):
- * - ISO10126d2Padding
- * - ISO7816d4Padding
- * - PKCS7Padding
- * - TBCPadding
- * - X923Padding
- * - ZeroBytePadding
- */
- BlockCipherPadding blockCipherPadding = new ZeroBytePadding();
- this.cipher = new PaddedBufferedBlockCipher(engine, blockCipherPadding);
-
-// logger.debug("Encryption cipher: " + cipher.getProvider().getInfo());
- }
-
- protected byte[] process(boolean toEncrypt, byte[] input)
- {
- try
- {
- //CipherParameters param = new KeyParameter(keyProvider.getKey());
- //cipher.init(toEncrypt, param);
-
- int inputLength = input.length;
- int maximumOutputLength = cipher.getOutputSize(inputLength);
- byte[] output = new byte[maximumOutputLength];
-
- int outputOffset = 0;
- int outputLength = 0;
- int bytesProcessed = cipher.processBytes(input, 0, input.length, output, 0);
- outputOffset += bytesProcessed;
- outputLength += bytesProcessed;
- bytesProcessed = cipher.doFinal(output, outputOffset);
- outputOffset += bytesProcessed;
- outputLength += bytesProcessed;
-
- if(outputLength == output.length)
- {
- return output;
- }
- else
- {
- byte[] truncatedOutput = new byte[outputLength];
- System.arraycopy(output, 0, truncatedOutput, 0, outputLength);
- return truncatedOutput;
- }
- }
- catch(InvalidCipherTextException ex)
- {
- throw new AlfrescoRuntimeException("Unexpected encryption error", ex);
- }
- }
-
- public byte[] encrypt(byte[] input)
- {
- return process(true, input);
- }
-
- public byte[] decrypt(byte[] input)
- {
- return process(false, input);
- }
-
- public byte[] encryptString(String input) throws UnsupportedEncodingException
- {
- byte[] in = input.getBytes("UTF-8");
- return encrypt(in);
- }
-
- public String decryptAsString(byte[] input) throws UnsupportedEncodingException
- {
- return new String(decrypt(input), "UTF-8").trim();
- }
-
-}
diff --git a/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngineTest.java b/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngineTest.java
deleted file mode 100644
index a8f97bcae0..0000000000
--- a/source/java/org/alfresco/repo/security/encryption/DefaultEncryptionEngineTest.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.alfresco.repo.security.encryption;
-
-import java.io.UnsupportedEncodingException;
-
-import junit.framework.TestCase;
-
-public class DefaultEncryptionEngineTest extends TestCase
-{
- private DefaultEncryptionEngine encryptionEngine;
-
- public void setUp() throws Exception
- {
- encryptionEngine = new DefaultEncryptionEngine();
- encryptionEngine.setKeyProvider(new TestKeyProvider());
- }
-
- public void testBasic()
- {
- try
- {
- String testString = "Hello World";
-
- byte[] bytes = encryptionEngine.encryptString(testString);
- String output = encryptionEngine.decryptAsString(bytes);
- assertEquals("", testString, output);
- }
- catch(UnsupportedEncodingException ex)
- {
- fail("Unexpected exception: " + ex);
- }
- }
-}
diff --git a/source/java/org/alfresco/repo/security/encryption/DefaultEncryptor.java b/source/java/org/alfresco/repo/security/encryption/DefaultEncryptor.java
new file mode 100644
index 0000000000..a59414844a
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/encryption/DefaultEncryptor.java
@@ -0,0 +1,75 @@
+package org.alfresco.repo.security.encryption;
+
+import java.security.AlgorithmParameters;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+
+import org.alfresco.util.PropertyCheck;
+
+/**
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class DefaultEncryptor extends AbstractEncryptor
+{
+ private String cipherAlgorithm;
+ private String cipherProvider;
+
+ private final ThreadLocal threadCipher;
+
+ /**
+ * Default constructor for IOC
+ */
+ public DefaultEncryptor()
+ {
+ threadCipher = new ThreadLocal();
+ }
+
+ /**
+ * Convenience constructor for tests
+ */
+ /* package */ DefaultEncryptor(KeyProvider keyProvider, String cipherAlgorithm, String cipherProvider)
+ {
+ this();
+ setKeyProvider(keyProvider);
+ setCipherAlgorithm(cipherAlgorithm);
+ setCipherProvider(cipherProvider);
+ }
+
+ public void setCipherAlgorithm(String cipherAlgorithm)
+ {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ public void setCipherProvider(String cipherProvider)
+ {
+ this.cipherProvider = cipherProvider;
+ }
+
+ public void init()
+ {
+ super.init();
+ PropertyCheck.mandatory(this, "cipherAlgorithm", cipherAlgorithm);
+ }
+
+ @Override
+ protected Cipher getCipher(Key key, AlgorithmParameters params, int mode) throws Exception
+ {
+ Cipher cipher = threadCipher.get();
+ if (cipher == null)
+ {
+ if (cipherProvider == null)
+ {
+ cipher = Cipher.getInstance(cipherAlgorithm);
+ }
+ else
+ {
+ cipher = Cipher.getInstance(cipherAlgorithm, cipherProvider);
+ }
+ threadCipher.set(cipher);
+ }
+ cipher.init(mode, key, params);
+ return cipher;
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/encryption/EncryptionEngine.java b/source/java/org/alfresco/repo/security/encryption/EncryptionEngine.java
deleted file mode 100644
index 46a45353ab..0000000000
--- a/source/java/org/alfresco/repo/security/encryption/EncryptionEngine.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.alfresco.repo.security.encryption;
-
-import java.io.UnsupportedEncodingException;
-
-
-public interface EncryptionEngine
-{
- public byte[] encrypt(byte[] input);
- public byte[] decrypt(byte[] input);
- public byte[] encryptString(String input) throws UnsupportedEncodingException;
- public String decryptAsString(byte[] input) throws UnsupportedEncodingException;
-}
diff --git a/source/java/org/alfresco/repo/security/encryption/Encryptor.java b/source/java/org/alfresco/repo/security/encryption/Encryptor.java
new file mode 100644
index 0000000000..ff5d075692
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/encryption/Encryptor.java
@@ -0,0 +1,86 @@
+package org.alfresco.repo.security.encryption;
+
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+
+import javax.crypto.Cipher;
+import javax.crypto.SealedObject;
+
+import org.alfresco.util.Pair;
+
+
+/**
+ * Interface providing methods to encrypt and decrypt data.
+ *
+ * @since 4.0
+ */
+public interface Encryptor
+{
+ /**
+ * Get the basic cipher that must be used for the given use-case
+ *
+ * @param keyAlias the encryption key alias
+ * @param params the parameters for the encryption or decryption
+ * @param mode the encryption mode
+ * @return the cipher to use or null if there is no
+ * key associated with the key alias
+ */
+ Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode);
+
+ /**
+ * Encrypt some bytes
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to encrypt
+ * @return the encrypted data and parameters used
+ */
+ Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Decrypt some bytes
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to decrypt
+ * @return the unencrypted data
+ */
+ byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Encrypt an object
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to write to bytes
+ * @return the encrypted data and parameters used
+ */
+ Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input);
+
+ /**
+ * Decrypt data as an object
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to decrypt
+ * @return the unencrypted data deserialized
+ */
+ Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Convenience method to seal on object up cryptographically
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to encrypt and seal
+ * @return the sealed object that can be decrypted with the original key
+ */
+ SealedObject sealObject(String keyAlias, AlgorithmParameters params, Serializable input);
+
+ /**
+ * Convenience method to unseal on object up cryptographically.
+ *
+ * Note that the algorithm parameters are stored in the sealed object and are
+ * not therefore required for decryption.
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to decrypt and unseal
+ * @return the original unsealed object that was encrypted with the original key
+ */
+ Serializable unsealObject(String keyAlias, SealedObject input);
+}
diff --git a/source/java/org/alfresco/repo/security/encryption/EncryptorTest.java b/source/java/org/alfresco/repo/security/encryption/EncryptorTest.java
new file mode 100644
index 0000000000..e60a9f1f4d
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/encryption/EncryptorTest.java
@@ -0,0 +1,61 @@
+package org.alfresco.repo.security.encryption;
+
+import java.security.AlgorithmParameters;
+
+import junit.framework.TestCase;
+
+import org.alfresco.util.Pair;
+import org.bouncycastle.util.Arrays;
+
+/**
+ * @since 4.0
+ */
+public class EncryptorTest extends TestCase
+{
+ private DefaultEncryptor encryptor;
+
+ public void setUp() throws Exception
+ {
+ encryptor = new DefaultEncryptor(
+ KeyStoreKeyProviderTest.getTestKeyStoreProvider(),
+ "DESede/CBC/PKCS5Padding",
+ null);
+ encryptor.init(); // Not currently necessary
+ }
+
+ public void testBasicBytes_NoKey()
+ {
+ byte[] bytes = new byte[] {11, 12, 13};
+
+ Pair encryptedPair = encryptor.encrypt("fluff", null, bytes);
+ byte[] decrypted = encryptor.decrypt(
+ "fluff",
+ encryptedPair.getSecond(),
+ encryptedPair.getFirst());
+ assertTrue("Encryption round trip failed. ", Arrays.areEqual(bytes, decrypted));
+ }
+
+ public void testBasicBytes_WithKey()
+ {
+ byte[] bytes = new byte[] {11, 12, 13};
+
+ Pair encryptedPair = encryptor.encrypt("mykey1", null, bytes);
+ byte[] decrypted = encryptor.decrypt(
+ "mykey1",
+ encryptedPair.getSecond(),
+ encryptedPair.getFirst());
+ assertTrue("Encryption round trip failed. ", Arrays.areEqual(bytes, decrypted));
+ }
+
+ public void testBasicObject()
+ {
+ Object testObject = " This is a string, but will be serialized ";
+
+ Pair encryptedPair = encryptor.encryptObject("mykey2", null, testObject);
+ Object output = encryptor.decryptObject(
+ "mykey2",
+ encryptedPair.getSecond(),
+ encryptedPair.getFirst());
+ assertEquals("Encryption round trip failed. ", testObject, output);
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/encryption/KeyProvider.java b/source/java/org/alfresco/repo/security/encryption/KeyProvider.java
index 86405777d1..044d65a9f0 100644
--- a/source/java/org/alfresco/repo/security/encryption/KeyProvider.java
+++ b/source/java/org/alfresco/repo/security/encryption/KeyProvider.java
@@ -3,11 +3,37 @@ package org.alfresco.repo.security.encryption;
import java.security.Key;
/**
- * A key provider returns the secret key used to encrypt text and mltext properties in the
- * database.
- *
+ * A key provider returns the secret keys for different use cases.
+ *
+ * @since 4.0
*/
public interface KeyProvider
{
- public Key getKey();
+ /**
+ * Enumeration of key aliases supported internally by Alfresco
+ *
+ * @author derekh
+ * @since 4.0
+ */
+ public static enum AlfrescoKeyAlias
+ {
+ METADATA,
+ SOLR
+ }
+
+ /**
+ * Get an encryption key if available.
+ *
+ * @param keyAlias the key alias
+ * @return the encryption key or null if there is no associated key
+ */
+ public Key getKey(String keyAlias);
+
+ /**
+ * Get an encryption key if available, using a convenience constant.
+ *
+ * @param keyAlias the key alias
+ * @return the encryption key or null if there is no associated key
+ */
+ public Key getKey(AlfrescoKeyAlias keyAlias);
}
diff --git a/source/java/org/alfresco/repo/security/encryption/KeyStoreKeyProviderTest.java b/source/java/org/alfresco/repo/security/encryption/KeyStoreKeyProviderTest.java
new file mode 100644
index 0000000000..da03c05c0c
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/encryption/KeyStoreKeyProviderTest.java
@@ -0,0 +1,161 @@
+package org.alfresco.repo.security.encryption;
+
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.UnrecoverableKeyException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.ApplicationContextHelper;
+import org.springframework.context.ApplicationContext;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests {@link KeystoreKeyProvider}
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class KeyStoreKeyProviderTest extends TestCase
+{
+ private static final String FILE_ONE = "classpath:alfresco/keystore-tests/ks-test-1.jks";
+ private static final String FILE_TWO = "classpath:alfresco/keystore-tests/ks-test-2.jks";
+ private static final String FILE_THREE = "classpath:alfresco/keystore-tests/ks-test-3.jks";
+ private static final String ALIAS_ONE = "mykey1";
+ private static final String ALIAS_TWO = "mykey2";
+ private static final String ALIAS_THREE = "mykey3";
+
+ public void setUp() throws Exception
+ {
+ }
+
+ /**
+ * Helper utility to create a two-alias keystore.
+ */
+ /* package */ static KeystoreKeyProvider getTestKeyStoreProvider()
+ {
+ Map passwords = new HashMap(5);
+ passwords.put(KeystoreKeyProvider.KEY_KEYSTORE_PASSWORD, "ksPwd2");
+ passwords.put(ALIAS_ONE, "aliasPwd1");
+ passwords.put(ALIAS_TWO, "aliasPwd2");
+ KeystoreKeyProvider ks = new KeystoreKeyProvider(
+ FILE_TWO,
+ "SunJCE",
+ "JCEKS",
+ passwords);
+ return ks;
+ }
+
+ public void testNoKeyStorePasswords() throws Exception
+ {
+ KeystoreKeyProvider keyProvider = new KeystoreKeyProvider(
+ FILE_ONE,
+ "SunJCE",
+ "JCEKS",
+ Collections.emptyMap());
+ // This has succeeded because we have not attempted to access it
+ assertNull("Should be no keys available", keyProvider.getKey(ALIAS_ONE));
+ }
+
+ public void testKeyStoreWithOnlyAliasPasswords() throws Exception
+ {
+ KeystoreKeyProvider keyProvider = new KeystoreKeyProvider(
+ FILE_TWO,
+ "SunJCE",
+ "JCEKS",
+ Collections.singletonMap(ALIAS_ONE, "aliasPwd1"));
+ // This has succeeded because we have not attempted to access it
+ assertNotNull("Should be able to key alias with same password", keyProvider.getKey(ALIAS_ONE));
+ }
+
+ public void testAliasWithIncorrectPassword_One() throws Exception
+ {
+ try
+ {
+ new KeystoreKeyProvider(
+ FILE_ONE,
+ "SunJCE",
+ "JCEKS",
+ Collections.singletonMap(ALIAS_ONE, "password_fail"));
+ fail("Expect to fail because password is incorrect");
+ }
+ catch (AlfrescoRuntimeException e)
+ {
+ // Expected
+ assertTrue(e.getCause() instanceof UnrecoverableKeyException);
+ }
+ }
+
+ public void testAliasWithIncorrectPassword_Two() throws Exception
+ {
+ try
+ {
+ new KeystoreKeyProvider(
+ FILE_TWO,
+ "SunJCE",
+ "JCEKS",
+ Collections.singletonMap(ALIAS_TWO, "password_fail"));
+ fail("Expect to fail because password is incorrect");
+ }
+ catch (AlfrescoRuntimeException e)
+ {
+ // Expected
+ assertTrue(e.getCause() instanceof UnrecoverableKeyException);
+ }
+ }
+
+ public void testAliasWithCorrectPassword_One() throws Exception
+ {
+ KeystoreKeyProvider ks = new KeystoreKeyProvider(
+ FILE_ONE,
+ "SunJCE",
+ "JCEKS",
+ Collections.singletonMap(ALIAS_ONE, "aliasPwd1"));
+ Key keyOne = ks.getKey(ALIAS_ONE);
+ assertNotNull(keyOne);
+ }
+
+ public void testAliasWithCorrectPassword_Two() throws Exception
+ {
+ Map passwords = new HashMap(5);
+ passwords.put(ALIAS_ONE, "aliasPwd1");
+ passwords.put(ALIAS_TWO, "aliasPwd2");
+ KeystoreKeyProvider ks = new KeystoreKeyProvider(
+ FILE_TWO,
+ "SunJCE",
+ "JCEKS",
+ passwords);
+ assertNotNull(ks.getKey(ALIAS_ONE));
+ assertNotNull(ks.getKey(ALIAS_TWO));
+ }
+
+ public void testAliasWithCorrectPassword_Three() throws Exception
+ {
+ Map passwords = new HashMap(5);
+ passwords.put(ALIAS_ONE, "aliasPwd1");
+ passwords.put(ALIAS_TWO, "aliasPwd2");
+ passwords.put(ALIAS_THREE, "aliasPwd3");
+ KeystoreKeyProvider ks = new KeystoreKeyProvider(
+ FILE_THREE,
+ "SunJCE",
+ "JCEKS",
+ passwords);
+ assertNotNull(ks.getKey(ALIAS_ONE));
+ assertNotNull(ks.getKey(ALIAS_TWO));
+ assertNull(ks.getKey(ALIAS_THREE));
+ }
+
+ /**
+ * TODO: Do we need spring-crypto when it is V1.0?
+ */
+ public void DISABLED_testSpringCrypto() throws Throwable
+ {
+ ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(
+ new String[] {"alfresco/keystore-tests/encryption-test-context.xml"});
+ @SuppressWarnings("unused")
+ KeyStore ks1 = (KeyStore) ctx.getBean("ks-test-1");
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/encryption/KeystoreKeyProvider.java b/source/java/org/alfresco/repo/security/encryption/KeystoreKeyProvider.java
index c8ff17131b..00848060f8 100644
--- a/source/java/org/alfresco/repo/security/encryption/KeystoreKeyProvider.java
+++ b/source/java/org/alfresco/repo/security/encryption/KeystoreKeyProvider.java
@@ -1,171 +1,246 @@
package org.alfresco.repo.security.encryption;
-import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-
-import javax.crypto.SecretKey;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.alfresco.error.AlfrescoRuntimeException;
-import org.alfresco.util.ParameterCheck;
+import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.util.ResourceUtils;
/**
*
* Provides the system-wide secret key for symmetric database encryption from a key store
* in the filesystem.
*
+ * @author Derek Hulley
+ * @since 4.0
*/
public class KeystoreKeyProvider extends AbstractKeyProvider
{
+ public static final String KEY_KEYSTORE_PASSWORD = "keystore";
+
private static final Log logger = LogFactory.getLog(KeyProvider.class);
- private static String KEY_STORE_TYPE = "JCEKS";
- private static String SECRET_KEY_ALIAS = "secret";
-
- // key store holding the secret key for encrypting and decrypting repository properties
- private String keyStoreFile;
-
- // key store passwords
- private char[] keyStorePassword;
- private char[] secretKeyPassword;
-
- public void setKeyStoreFile(String keyStoreFile)
- {
- this.keyStoreFile = keyStoreFile;
- }
-
- public void setKeyStorePassword(String keyStorePassword)
- {
- this.keyStorePassword = keyStorePassword.toCharArray();
- }
+ // Will be cleared after initialization
+ private Map passwords;
+ private String location;
+ private String provider;
+ private String type;
+ private Map keys;
- public void setSecretKeyPassword(String secretKeyPassword)
- {
- this.secretKeyPassword = secretKeyPassword.toCharArray();
- }
-
- public Key getKey()
- {
- return super.getKey();
- }
+ private final ReadLock readLock;
+ private final WriteLock writeLock;
- protected void saveKeyStore(KeyStore ks) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException
- {
- FileOutputStream fos = null;
-
- try
- {
- fos = new FileOutputStream(keyStoreFile);
- ks.store(fos, keyStorePassword);
- }
- finally
- {
- if(fos != null)
- {
- fos.close();
- }
- }
- }
-
- /*
- * Create a new secret key and store it in the keystore ks
+ /**
+ * Constructs the provider with required defaults
*/
- protected void createSecretKey(KeyStore ks) throws Exception
+ public KeystoreKeyProvider()
{
- Key key = generateSecretKey();
- if(key == null)
- {
- throw new AlfrescoRuntimeException("Unable to generate secret key");
- }
-
- byte[] encoded = key.getEncoded();
-
- logger.debug("secret key size = " + (encoded.length * 8) + " bits");
-
- KeyStore.SecretKeyEntry skEntry = new KeyStore.SecretKeyEntry((SecretKey)key);
-
- ks.setEntry(SECRET_KEY_ALIAS, skEntry, new KeyStore.PasswordProtection(secretKeyPassword));
-
- saveKeyStore(ks);
+ ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ readLock = lock.readLock();
+ writeLock = lock.writeLock();
+ keys = new HashMap(7);
}
- protected void loadKeyStore()
+ /**
+ * Convenience constructor for tests. Note that {@link #init()} is also called.
+ */
+ /* package */ KeystoreKeyProvider(String location, String provider, String type, Map passwords)
{
- InputStream is = null;
- KeyStore ks = null;
-
- try
- {
- ks = KeyStore.getInstance(KEY_STORE_TYPE);
-
- File f = new File(keyStoreFile);
- if(!f.exists())
- {
- // no keystore, create one and save it
- ks.load(null, keyStorePassword);
-
- // generate a secret key
- createSecretKey(ks);
- }
- else
- {
- is = new BufferedInputStream(new FileInputStream(keyStoreFile));
- ks.load(is, keyStorePassword);
- }
- }
- catch(Throwable e)
- {
- throw new AlfrescoRuntimeException(
- "Unable to load keystore from " + keyStoreFile, e);
- }
- finally
- {
- if (is != null)
-
- {
- try
- {
- is.close();
- }
- catch(IOException e)
- {
- }
- }
- }
+ this();
+ setLocation(location);
+ setProvider(provider);
+ setType(type);
+ setPasswords(passwords);
+ init();
+ }
- try
- {
- Key key = ks.getKey(SECRET_KEY_ALIAS, secretKeyPassword);
- if(key == null)
- {
- createSecretKey(ks);
- }
+ public void setLocation(String location)
+ {
+ this.location = location;
+ }
- setKey(key);
- }
- catch(Throwable e)
- {
- throw new AlfrescoRuntimeException(
- "Unable to get secret key from " + keyStoreFile, e);
- }
+ public void setProvider(String provider)
+ {
+ this.provider = provider;
+ }
+
+ public void setType(String type)
+ {
+ this.type = type;
+ }
+
+ /**
+ * Set the map of passwords to access the keystore.
+ *
+ * Where required, null values must be inserted into the map to indicate the presence
+ * of a key that is not protected by a password. They entry for {@link #KEY_KEYSTORE_PASSWORD}
+ * is required if the keystore is password protected.
+ *
+ * @param passwords a map of passwords including null values
+ */
+ public void setPasswords(Map passwords)
+ {
+ this.passwords = new HashMap(passwords);
}
public void init()
{
- ParameterCheck.mandatory("keyStoreFile", keyStoreFile);
- ParameterCheck.mandatory("passwordGenerator", passwordGenerator);
+ writeLock.lock();
+ try
+ {
+ safeInit();
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Initializes class; must be done in a write lock.
+ */
+ private void safeInit()
+ {
+ if (!PropertyCheck.isValidPropertyString(location))
+ {
+ location = null;
+ }
+ if (!PropertyCheck.isValidPropertyString(provider))
+ {
+ provider = null;
+ }
+ if (!PropertyCheck.isValidPropertyString(type))
+ {
+ type = null;
+ }
+
+ PropertyCheck.mandatory(this, "location", location);
+ // Extract the keystore password
+ String pwdKeyStore = passwords.get(KEY_KEYSTORE_PASSWORD);
- loadKeyStore();
+ // Make sure we choose the default type, if required
+ if (type == null)
+ {
+ type = KeyStore.getDefaultType();
+ }
+
+ KeyStore ks = null;
+ InputStream is = null;
+ try
+ {
+ if (provider == null)
+ {
+ ks = KeyStore.getInstance(type);
+ }
+ else
+ {
+ ks = KeyStore.getInstance(type, provider);
+ }
+ // Load it up
+ File ksFile = ResourceUtils.getFile(location);
+ if (!ksFile.exists())
+ {
+ throw new IOException("Unable to find keystore file: " + ksFile);
+ }
+ is = new FileInputStream(ksFile);
+ ks.load(is, pwdKeyStore == null ? null : pwdKeyStore.toCharArray());
+ // Loaded
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Initialize keystore provider: \n" +
+ " Location: " + location + "\n" +
+ " Provider: " + provider + "\n" +
+ " Type: " + type);
+ }
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to initialize keystore provider: \n" +
+ " Location: " + location + "\n" +
+ " Provider: " + provider + "\n" +
+ " Type: " + type,
+ e);
+ }
+ finally
+ {
+ pwdKeyStore = null;
+ passwords.remove(KEY_KEYSTORE_PASSWORD);
+ if (is != null)
+ {
+ try { is.close(); } catch (Throwable e) {}
+ }
+ }
+
+ // Now get the other keys
+ for (Map.Entry element : passwords.entrySet())
+ {
+ String keyAlias = element.getKey();
+ String passwordStr = element.getValue();
+ if (!PropertyCheck.isValidPropertyString(passwordStr))
+ {
+ // Force a failure because the property was not properly initialized
+ PropertyCheck.mandatory(this, "passwords." + keyAlias, null);
+ }
+ // Null is an acceptable value (means no key)
+ Key key = null;
+ // Attempt to key the key
+ try
+ {
+ key = ks.getKey(keyAlias, passwordStr == null ? null : passwordStr.toCharArray());
+ keys.put(keyAlias, key);
+ // Key loaded
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Retrieved key from keystore: \n" +
+ " Location: " + location + "\n" +
+ " Provider: " + provider + "\n" +
+ " Type: " + type + "\n" +
+ " Alias: " + keyAlias + "\n" +
+ " Password?: " + (passwordStr != null));
+ }
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to retrieve key from keystore: \n" +
+ " Location: " + location + "\n" +
+ " Provider: " + provider + "\n" +
+ " Type: " + type + "\n" +
+ " Alias: " + keyAlias + "\n" +
+ " Password?: " + (passwordStr != null),
+ e);
+ }
+ }
+ // Clear passwords
+ passwords.clear();
}
+ @Override
+ public Key getKey(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ return keys.get(keyAlias);
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
}
diff --git a/source/java/org/alfresco/repo/security/encryption/TestKeyProvider.java b/source/java/org/alfresco/repo/security/encryption/TestKeyProvider.java
deleted file mode 100644
index 3bcb6a8fb9..0000000000
--- a/source/java/org/alfresco/repo/security/encryption/TestKeyProvider.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.alfresco.repo.security.encryption;
-
-import java.security.Key;
-
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-
-import org.alfresco.error.AlfrescoRuntimeException;
-
-public class TestKeyProvider implements KeyProvider
-{
- public Key getKey()
- {
- try
- {
- KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
- SecretKey key = keyGenerator.generateKey();
- return key;
- // return Hex.decode("80000000000000000000000000000000");
- }
- catch(Exception e)
- {
- throw new AlfrescoRuntimeException("Unexpected exception generating secret key", e);
- }
- }
-}
diff --git a/source/test-resources/alfresco/keystore-tests/encryption-test-context.xml b/source/test-resources/alfresco/keystore-tests/encryption-test-context.xml
new file mode 100644
index 0000000000..a28492b7e3
--- /dev/null
+++ b/source/test-resources/alfresco/keystore-tests/encryption-test-context.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/source/test-resources/alfresco/keystore-tests/ks-test-1.jks b/source/test-resources/alfresco/keystore-tests/ks-test-1.jks
new file mode 100644
index 0000000000..01bd383454
Binary files /dev/null and b/source/test-resources/alfresco/keystore-tests/ks-test-1.jks differ
diff --git a/source/test-resources/alfresco/keystore-tests/ks-test-2.jks b/source/test-resources/alfresco/keystore-tests/ks-test-2.jks
new file mode 100644
index 0000000000..3723222500
Binary files /dev/null and b/source/test-resources/alfresco/keystore-tests/ks-test-2.jks differ
diff --git a/source/test-resources/alfresco/keystore-tests/ks-test-3.jks b/source/test-resources/alfresco/keystore-tests/ks-test-3.jks
new file mode 100644
index 0000000000..855192ebdd
Binary files /dev/null and b/source/test-resources/alfresco/keystore-tests/ks-test-3.jks differ
diff --git a/source/test-resources/alfresco/keystore-tests/ks-test-4.jks b/source/test-resources/alfresco/keystore-tests/ks-test-4.jks
new file mode 100644
index 0000000000..0e5c097f70
Binary files /dev/null and b/source/test-resources/alfresco/keystore-tests/ks-test-4.jks differ
diff --git a/source/test-resources/alfresco/keystore-tests/passwords.txt b/source/test-resources/alfresco/keystore-tests/passwords.txt
new file mode 100644
index 0000000000..9a3aba0085
--- /dev/null
+++ b/source/test-resources/alfresco/keystore-tests/passwords.txt
@@ -0,0 +1,37 @@
+The keystores contained in this folder are used by tests.
+There are their details:
+
+==================
+== ks-test-1.ks ==
+==================
+
+Keystore password: ksPwd1
+Keystore type: JCEKS
+Keystore provider: SunJCE
+
+Your keystore contains 1 entry
+mykey1: aliasPwd1
+
+==================
+== ks-test-2.ks ==
+==================
+
+Keystore password: ksPwd2
+Keystore type: JCEKS
+Keystore provider: SunJCE
+
+Your keystore contains 2 entries
+mykey1: aliasPwd1
+mykey2: aliasPwd2
+
+==================
+== ks-test-3.ks ==
+==================
+
+Keystore password: ksPwd3
+Keystore type: JCEKS
+Keystore provider: SunJCE
+
+Your keystore contains 2 entries
+mykey1: aliasPwd1
+mykey2: aliasPwd2
\ No newline at end of file