Fix for ALF-10189:

o Alfresco key store manages keys and backup keys internally
  o moved key registration and checking into AlfrescoKeyStoreImpl
  o encryptor thread cache fix resulting from reload of key stores at runtime
  o more encryption and key store tests
  o tidy up + more comments
  o moved hard-coded values to properties file

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@30405 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Steven Glover
2011-09-11 12:09:24 +00:00
parent 195ea9c810
commit 69d5e091e0
18 changed files with 1011 additions and 341 deletions

View File

@@ -52,7 +52,7 @@ public class BootstrapReEncryptor extends AbstractLifecycleBean
{
return reEncryptor.bootstrapReEncrypt();
}
catch(MissingKeyStoreException e)
catch(MissingKeyException e)
{
throw new AlfrescoRuntimeException("Bootstrap re-encryption failed", e);
}

View File

@@ -18,6 +18,9 @@
*/
package org.alfresco.encryption;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
@@ -37,31 +40,39 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean;
public class EncryptionChecker extends AbstractLifecycleBean
{
private static Log logger = LogFactory. getLog(EncryptionChecker.class);
private TransactionService transactionService;
private KeyStoreChecker keyStoreChecker;
private KeyStoreParameters keyStoreParameters;
private KeyResourceLoader keyResourceLoader;
public void setkeyStoreParameters(KeyStoreParameters keyStoreParameters)
{
this.keyStoreParameters = keyStoreParameters;
}
public void setKeyStoreChecker(KeyStoreChecker keyStoreChecker)
{
this.keyStoreChecker = keyStoreChecker;
}
public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader)
public void setTransactionService(TransactionService transactionService)
{
this.keyResourceLoader = keyResourceLoader;
this.transactionService = transactionService;
}
@Override
protected void onBootstrap(ApplicationEvent event)
{
AlfrescoKeyStore mainKeyStore = new AlfrescoKeyStoreImpl(keyStoreParameters, keyResourceLoader);
keyStoreChecker.checkKeyStore(mainKeyStore);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>()
{
public Void execute() throws Throwable
{
try
{
keyStoreChecker.validateKeyStores();
}
catch(Throwable e)
{
// Just throw as a runtime exception
throw new AlfrescoRuntimeException("Keystores are invalid", e);
}
return null;
}
});
}
@Override

View File

@@ -20,8 +20,10 @@ package org.alfresco.encryption;
import java.io.Serializable;
import java.security.InvalidKeyException;
import java.security.Key;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
@@ -34,6 +36,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Registered Encryption Keys are stored in the AttributeService directly under a top level key defined by
* TOP_LEVEL_KEY (which means that all key aliases must be unique across however many keystores are being used).
*
* @since 4.0
*
@@ -46,16 +50,22 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
private TransactionService transactionService;
private AttributeService attributeService;
private Encryptor encryptor;
private String cipherAlgorithm;
private String cipherProvider;
public void setAttributeService(AttributeService attributeService)
{
this.attributeService = attributeService;
}
public void setEncryptor(Encryptor encryptor)
public void setCipherAlgorithm(String cipherAlgorithm)
{
this.encryptor = encryptor;
this.cipherAlgorithm = cipherAlgorithm;
}
public void setCipherProvider(String cipherProvider)
{
this.cipherProvider = cipherProvider;
}
public void setTransactionService(TransactionService transactionService)
@@ -63,55 +73,87 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
this.transactionService = transactionService;
}
public void registerKey(String keyAlias)
protected Encryptor getEncryptor(final KeyMap keys)
{
DefaultEncryptor encryptor = new DefaultEncryptor();
encryptor.setCipherAlgorithm(cipherAlgorithm);
encryptor.setCipherProvider(cipherProvider);
encryptor.setKeyProvider(new KeyProvider()
{
@Override
public Key getKey(String keyAlias)
{
return keys.getCachedKey(keyAlias).getKey();
}
});
return encryptor;
}
public void init()
{
}
public void registerKey(String keyAlias, Key key)
{
if(isKeyRegistered(keyAlias))
{
throw new IllegalArgumentException("Key " + keyAlias + " is already registered");
}
// register the key by creating an attribute that stores a guid and its encrypted value
String guid = GUID.generate();
KeyMap keys = new KeyMap();
keys.setKey(keyAlias, key);
Encryptor encryptor = getEncryptor(keys);
Serializable encrypted = encryptor.sealObject(keyAlias, null, guid);
KeyCheck keyCheck = new KeyCheck(guid, encrypted);
attributeService.createAttribute(keyCheck, TOP_LEVEL_KEY, keyAlias);
logger.info("Registered key " + keyAlias);
}
public KeysReport getKeysReport()
public void unregisterKey(String keyAlias)
{
attributeService.removeAttribute(TOP_LEVEL_KEY, keyAlias);
}
public boolean isKeyRegistered(String keyAlias)
{
return (attributeService.getAttribute(TOP_LEVEL_KEY, keyAlias) != null);
}
public List<String> getRegisteredKeys(final Set<String> keyStoreKeys)
{
final List<String> registeredKeys = new ArrayList<String>();
if(attributeService.exists(TOP_LEVEL_KEY))
attributeService.getAttributes(new AttributeQueryCallback()
{
attributeService.getAttributes(new AttributeQueryCallback()
public boolean handleAttribute(Long id, Serializable value,
Serializable[] keys)
{
public boolean handleAttribute(Long id, Serializable value,
Serializable[] keys)
if(value instanceof KeyCheck)
{
registeredKeys.add((String)value);
return true;
// Add as a registered key if the keystore contains the key
String keyAlias = (String)keys[1];
if(keyStoreKeys.contains(keyAlias))
{
registeredKeys.add(keyAlias);
}
}
},
TOP_LEVEL_KEY);
}
List<String> keyAliasesChanged = new ArrayList<String>();
List<String> keyAliasesUnchanged = new ArrayList<String>();
for(String keyAlias : registeredKeys)
{
KEY_STATUS keyStatus = checkKey(keyAlias);
if(keyStatus == KEY_STATUS.CHANGED)
{
keyAliasesChanged.add(keyAlias);
else
{
logger.warn("Unexpected value class in keys registry: " + value.getClass());
}
return true;
}
else
{
keyAliasesUnchanged.add(keyAlias);
}
}
return new KeysReport(keyAliasesChanged, keyAliasesUnchanged);
},
TOP_LEVEL_KEY);
return registeredKeys;
}
public KEY_STATUS checkKey(String keyAlias)
public KEY_STATUS checkKey(String keyAlias, Key key)
{
if(attributeService.exists(TOP_LEVEL_KEY, keyAlias))
{
@@ -120,6 +162,10 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
// check that the key has not changed by decrypting the encrypted guid attribute
// comparing against the guid
KeyCheck keyCheck = (KeyCheck)attributeService.getAttribute(TOP_LEVEL_KEY, keyAlias);
KeyMap keys = new KeyMap();
keys.setKey(keyAlias, key);
Encryptor encryptor = getEncryptor(keys);
Serializable storedGUID = encryptor.unsealObject(keyAlias, keyCheck.getEncrypted());
return EqualsHelper.nullSafeEquals(storedGUID, keyCheck.getGuid()) ? KEY_STATUS.OK : KEY_STATUS.CHANGED;
}
@@ -137,14 +183,14 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
}
// note that this removes _all_ keys in the keystore. Use with care.
public void removeRegisteredKeys(final AlfrescoKeyStore keyStore)
public void removeRegisteredKeys(final Set<String> keys)
{
RetryingTransactionHelper retryingTransactionHelper = transactionService.getRetryingTransactionHelper();
final RetryingTransactionCallback<Void> removeKeysCallback = new RetryingTransactionCallback<Void>()
{
public Void execute() throws Throwable
{
for(String keyAlias : keyStore.getKeyAliases())
for(String keyAlias : keys)
{
attributeService.removeAttribute(TOP_LEVEL_KEY, keyAlias);
}
@@ -161,7 +207,7 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
* @since 4.0
*
*/
private static class KeyCheck implements Serializable
public static class KeyCheck implements Serializable
{
private static final long serialVersionUID = 4514315444977162903L;
@@ -201,4 +247,5 @@ public class EncryptionKeysRegistryImpl implements EncryptionKeysRegistry
EqualsHelper.nullSafeEquals(keyCheck.getEncrypted(), getEncrypted());
}
}
}

View File

@@ -18,8 +18,8 @@
*/
package org.alfresco.encryption;
import java.io.File;
import java.io.Serializable;
import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
@@ -51,6 +51,7 @@ import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.Pair;
import org.springframework.context.ApplicationContext;
public class EncryptionTests extends TestCase
@@ -72,7 +73,8 @@ public class EncryptionTests extends TestCase
private ReEncryptor reEncryptor;
private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
private KeyStoreParameters backupKeyStoreParameters;
private AlfrescoKeyStoreImpl backupKeyStore;
private AlfrescoKeyStoreImpl mainKeyStore;
//private AlfrescoKeyStoreImpl backupKeyStore;
private KeyResourceLoader keyResourceLoader;
private EncryptionKeysRegistryImpl encryptionKeysRegistry;
private KeyStoreChecker keyStoreChecker;
@@ -84,7 +86,7 @@ public class EncryptionTests extends TestCase
private TenantService tenantService;
private String keyAlgorithm;
private Map<String, Key> newKeys = new HashMap<String, Key>();
private KeyMap newKeys = new KeyMap();
private List<NodeRef> before = new ArrayList<NodeRef>();
private List<NodeRef> after = new ArrayList<NodeRef>();
@@ -104,8 +106,9 @@ public class EncryptionTests extends TestCase
backupKeyStoreParameters = (KeyStoreParameters)ctx.getBean("backupKeyStoreParameters");
keyStoreChecker = (KeyStoreChecker)ctx.getBean("keyStoreChecker");
encryptionKeysRegistry = (EncryptionKeysRegistryImpl)ctx.getBean("encryptionKeysRegistry");
backupKeyStore = (AlfrescoKeyStoreImpl)ctx.getBean("backupKeyStore");
mainEncryptor = (DefaultEncryptor)ctx.getBean("encryptor");
//backupKeyStore = (AlfrescoKeyStoreImpl)ctx.getBean("backupKeyStore");
mainKeyStore = (AlfrescoKeyStoreImpl)ctx.getBean("keyStore");
mainEncryptor = (DefaultEncryptor)ctx.getBean("mainEncryptor");
backupEncryptor = (DefaultEncryptor)ctx.getBean("backupEncryptor");
// reencrypt in one txn (since we don't commit the model, the qnames won't be available across transactions)
@@ -122,8 +125,8 @@ public class EncryptionTests extends TestCase
rootNodeRef = nodeService.getRootNode(storeRef);
keyAlgorithm = "DESede";
newKeys.put(KeyProvider.ALIAS_METADATA, generateSecretKey(keyAlgorithm));
newKeys.setKey(KeyProvider.ALIAS_METADATA, generateSecretKey(keyAlgorithm));
// Load models
DictionaryBootstrap bootstrap = new DictionaryBootstrap();
List<String> bootstrapModels = new ArrayList<String>();
@@ -156,46 +159,19 @@ public class EncryptionTests extends TestCase
super.tearDown();
}
protected KeyProvider getKeyProvider(final Map<String, Key> keys)
protected KeyProvider getKeyProvider(final KeyMap keys)
{
KeyProvider keyProvider = new KeyProvider()
{
@Override
public Key getKey(String keyAlias)
{
return keys.get(keyAlias);
}
@Override
public void refresh()
{
// nothing to do
return keys.getCachedKey(keyAlias).getKey();
}
};
return keyProvider;
}
protected Encryptor getFallbackEncryptor(KeyProvider keyProvider)
{
DefaultEncryptor encryptor = new DefaultEncryptor();
encryptor.setCipherAlgorithm(cipherAlgorithm);
encryptor.setCipherProvider(null);
encryptor.setKeyProvider(keyProvider);
DefaultFallbackEncryptor fallbackEncryptor = new DefaultFallbackEncryptor(encryptor, mainEncryptor);
return fallbackEncryptor;
}
protected MetadataEncryptor getMetadataEncryptor(Encryptor encryptor)
{
MetadataEncryptor metadataEncryptor = new MetadataEncryptor();
metadataEncryptor.setDictionaryService(dictionaryService);
metadataEncryptor.setEncryptor(encryptor);
return metadataEncryptor;
}
protected void createEncryptedProperties(List<NodeRef> nodes)
{
for(int i = 0; i < NUM_PROPERTIES; i++)
@@ -269,7 +245,7 @@ public class EncryptionTests extends TestCase
assertEquals("", nodeRef.toString(), props.get(PROP));
}
}
catch(MissingKeyStoreException e)
catch(MissingKeyException e)
{
fail(e.getMessage());
}
@@ -294,36 +270,142 @@ public class EncryptionTests extends TestCase
{
// ensure that the backup key store is not available
backupKeyStoreParameters.setLocation("");
backupKeyStore.reload();
//backupKeyStore.reload();
mainKeyStore.reload();
reEncryptor.bootstrapReEncrypt();
fail("Should have caught missing backup key store");
}
catch(MissingKeyStoreException e)
catch(MissingKeyException e)
{
System.out.println("Successfully caught missing key store exception");
System.out.println("Successfully caught missing key exception");
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
}
protected void testChangeKeysImpl(boolean cacheCiphers) throws Throwable
{
// on a single thread
// create an encryptor, encrypt a string, change encryptor keys, decrypt -> should result in Invalid Key
Pair<byte[], AlgorithmParameters> pair = null;
DefaultEncryptor encryptor = null;
Key secretKey1 = null;
Key secretKey2 = null;
String test = "hello world";
final KeyMap keys = new KeyMap();
byte[] decrypted = null;
String test1 = null;
secretKey1 = generateSecretKey("DESede");
keys.setKey("test", secretKey1);
KeyProvider keyProvider = new KeyProvider()
{
@Override
public Key getKey(String keyAlias)
{
return keys.getCachedKey(keyAlias).getKey();
}
};
encryptor = new DefaultEncryptor();
encryptor.setCipherAlgorithm("DESede/CBC/PKCS5Padding");
encryptor.setCipherProvider(null);
encryptor.setKeyProvider(keyProvider);
encryptor.setCacheCiphers(cacheCiphers);
pair = encryptor.encrypt("test", null, test.getBytes("UTF-8"));
decrypted = encryptor.decrypt("test", pair.getSecond(), pair.getFirst());
test1 = new String(decrypted, "UTF-8");
assertEquals("Expected encrypt,decrypt to end up with the original value", test, test1);
System.out.println("1:" + new String(decrypted, "UTF-8"));
secretKey2 = generateSecretKey("DESede");
keys.setKey("test", secretKey2);
assertNotNull(encryptor);
assertNotNull(pair);
try
{
decrypted = encryptor.decrypt("test", pair.getSecond(), pair.getFirst());
test1 = new String(decrypted, "UTF-8");
}
catch(AlfrescoRuntimeException e)
{
// ok - decryption failed expected.
}
}
public void testKeyStoreCreation()
public void testChangeKeys() throws Throwable
{
String keyStoreLocation = System.getProperty("user.dir") + File.separator + "encryption-tests.keystore";
File keyStoreFile = new File(keyStoreLocation);
if(keyStoreFile.exists())
testChangeKeysImpl(false);
}
public void testChangeKeysCachedCiphers() throws Throwable
{
testChangeKeysImpl(true);
}
public void testFailedEncryptionWithCachedCiphers() throws Throwable
{
Pair<byte[], AlgorithmParameters> pair = null;
DefaultEncryptor encryptor = null;
Key secretKey1 = null;
Key secretKey2 = null;
String test = "hello world";
final KeyMap keys = new KeyMap();
byte[] decrypted = null;
String test1 = null;
secretKey1 = generateSecretKey("DESede");
keys.setKey("test", secretKey1);
KeyProvider keyProvider = new KeyProvider()
{
assertTrue("", keyStoreFile.delete());
@Override
public Key getKey(String keyAlias)
{
return keys.getCachedKey(keyAlias).getKey();
}
};
encryptor = new DefaultEncryptor();
encryptor.setCipherAlgorithm("DESede/CBC/PKCS5Padding");
encryptor.setCipherProvider(null);
encryptor.setKeyProvider(keyProvider);
encryptor.setCacheCiphers(true);
pair = encryptor.encrypt("test", null, test.getBytes("UTF-8"));
secretKey2 = generateSecretKey("DESede");
keys.setKey("test", secretKey2);
assertNotNull(encryptor);
assertNotNull(pair);
try
{
decrypted = encryptor.decrypt("test", pair.getSecond(), pair.getFirst());
test1 = new String(decrypted, "UTF-8");
fail("Decryption should have failed");
}
catch(AlfrescoRuntimeException e)
{
// ok - decryption failed expected - changed key
}
KeyStoreParameters keyStoreParameters = new KeyStoreParameters();
keyStoreParameters.setLocation(keyStoreLocation);
keyStoreParameters.setKeyMetaDataFileLocation("classpath:org/alfresco/encryption/keystore-parameters.properties");
keyStoreParameters.setType("JCEKS");
AlfrescoKeyStore keyStore = new AlfrescoKeyStoreImpl(keyStoreParameters, keyResourceLoader);
encryptionKeysRegistry.removeRegisteredKeys(keyStore);
keyStoreChecker.checkKeyStore(keyStore);
assertNotNull("", keyStore.getKey("test"));
keys.setKey("test", secretKey1);
try
{
decrypted = encryptor.decrypt("test", pair.getSecond(), pair.getFirst());
test1 = new String(decrypted, "UTF-8");
}
catch(AlfrescoRuntimeException e)
{
fail("Expected decryption to work ok");
}
}
}

View File

@@ -85,15 +85,14 @@ public class EncryptorTest extends TestCase
Serializable testObject = " This is a string, but will be serialized ";
Serializable sealedObject = encryptor.sealObject("mykey2", null, testObject);
// try
// {
try
{
Object output = encryptor.unsealObject("mykey2", sealedObject);
assertEquals("Encryption round trip failed. ", testObject, output);
// }
// catch(KeyException e)
// {
// throw new AlfrescoRuntimeException("", e)
//; }
}
catch(KeyException e)
{
throw new AlfrescoRuntimeException("", e);
}
}
}

View File

@@ -18,18 +18,11 @@
*/
package org.alfresco.encryption;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Validates an Alfresco keystore by ensuring that it's registered keys have not changed.
*
* The keys are registered using the AttributeService and are stored in one level under TOP_LEVEL_KEY
* (so key aliases must be unique across however many keystores are being used).
* Checks the repository key stores.
*
* @since 4.0
*
@@ -37,80 +30,24 @@ import org.apache.commons.logging.LogFactory;
public class KeyStoreChecker
{
private static final Log logger = LogFactory.getLog(KeyStoreChecker.class);
private TransactionService transactionService;
private EncryptionKeysRegistryImpl encryptionKeysRegistry;
private AlfrescoKeyStore mainKeyStore;
public KeyStoreChecker()
{
}
public void setEncryptionKeysRegistry(EncryptionKeysRegistryImpl encryptionKeysRegistry)
public void setMainKeyStore(AlfrescoKeyStore mainKeyStore)
{
this.encryptionKeysRegistry = encryptionKeysRegistry;
this.mainKeyStore = mainKeyStore;
}
public void setTransactionService(TransactionService transactionService)
public void validateKeyStores() throws InvalidKeystoreException, MissingKeyException
{
this.transactionService = transactionService;
}
public KeyStoreChecker(EncryptionKeysRegistryImpl encryptionKeysRegistry)
{
this.encryptionKeysRegistry = encryptionKeysRegistry;
}
protected void createKeyStore(AlfrescoKeyStore keyStore)
{
keyStore.create();
// Register the key store keys
for(String keyAlias : keyStore.getKeyAliases())
mainKeyStore.validateKeys();
if(!mainKeyStore.exists())
{
encryptionKeysRegistry.registerKey(keyAlias);
mainKeyStore.create();
}
}
public void checkKeyStore(final AlfrescoKeyStore keyStore)
{
// TODO check that, if keystore exists, keys are registered
RetryingTransactionHelper retryingTransactionHelper = transactionService.getRetryingTransactionHelper();
final RetryingTransactionCallback<Void> checkKeysCallback = new RetryingTransactionCallback<Void>()
{
public Void execute() throws Throwable
{
KeysReport keysReport = encryptionKeysRegistry.getKeysReport();
// Check for the existence of a key store first
if(keyStore.exists())
{
// The keystore exists - check whether any keys have been changed
// find out which registered keys have changed
if(keysReport.getKeysChanged().size() > 0)
{
// Note: this will halt the application bootstrap.
throw new AlfrescoRuntimeException("The keys with aliases " + keysReport.getKeysChanged() + " have been changed, re-instate the previous keystore");
}
}
else
{
// keystore not found, check whether any keys have been registered
if(keysReport.getKeysChanged().size() + keysReport.getKeysUnchanged().size() > 0)
{
// Note: this will halt the application bootstrap.
throw new AlfrescoRuntimeException("Keys have already been registered, re-instate the previous keystore");
}
if(logger.isDebugEnabled())
{
logger.debug("Keystore not found, creating...");
}
createKeyStore(keyStore);
}
return null;
}
};
retryingTransactionHelper.doInTransaction(checkKeysCallback, false);
}
}

View File

@@ -0,0 +1,419 @@
/*
* Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.encryption;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.apache.commons.codec.binary.Base64;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
/**
*
* @since 4.0
*
*/
public class KeyStoreTests
{
private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
private TransactionService transactionService;
private KeyStoreChecker keyStoreChecker;
private EncryptionKeysRegistry encryptionKeysRegistry;
private UserTransaction txn = null;
private KeyResourceLoader keyResourceLoader;
private List<String> toDelete;
private DefaultEncryptor backupEncryptor;
@Before
public void setup() throws SystemException, NotSupportedException
{
transactionService = (TransactionService)ctx.getBean("transactionService");
keyStoreChecker = (KeyStoreChecker)ctx.getBean("keyStoreChecker");
encryptionKeysRegistry = (EncryptionKeysRegistry)ctx.getBean("encryptionKeysRegistry");
keyResourceLoader = (KeyResourceLoader)ctx.getBean("springKeyResourceLoader");
backupEncryptor = (DefaultEncryptor)ctx.getBean("backupEncryptor");
toDelete = new ArrayList<String>(10);
AuthenticationUtil.setRunAsUserSystem();
UserTransaction txn = transactionService.getUserTransaction();
txn.begin();
}
@After
public void teardown() throws IllegalStateException, SecurityException, SystemException
{
if(txn != null)
{
txn.rollback();
}
for(String guid : toDelete)
{
File file = new File(guid);
if(file.exists())
{
file.delete();
}
}
}
public String generateEncodedKey()
{
try
{
return Base64.encodeBase64String(generateKeyData());
}
catch(Throwable e)
{
fail("Unexpected exception: " + e.getMessage());
return null;
}
}
public byte[] generateKeyData() throws NoSuchAlgorithmException
{
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(System.currentTimeMillis());
byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN];
random.nextBytes(bytes);
return bytes;
}
protected String generateKeystoreName()
{
String guid = GUID.generate();
toDelete.add(guid);
return guid;
}
protected Key generateSecretKey(String keyAlgorithm)
{
try
{
DESedeKeySpec keySpec = new DESedeKeySpec(generateKeyData());
SecretKeyFactory kf = SecretKeyFactory.getInstance(keyAlgorithm);
SecretKey secretKey = kf.generateSecret(keySpec);
return secretKey;
}
catch(Throwable e)
{
fail("Unexpected exception: " + e.getMessage());
return null;
}
}
protected TestAlfrescoKeyStore getKeyStore(String name, String type, final Map<String, String> passwords, final Map<String, String> encodedKeyData,
String keyStoreLocation, String backupKeyStoreLocation)
{
KeyResourceLoader testKeyResourceLoader = new KeyResourceLoader()
{
@Override
public InputStream getKeyStore(String keyStoreLocation)
throws FileNotFoundException
{
return keyResourceLoader.getKeyStore(keyStoreLocation);
}
@Override
public Properties loadKeyMetaData(String keyMetaDataFileLocation)
throws IOException, FileNotFoundException
{
Properties p = new Properties();
p.put("keystore.password", "password");
StringBuilder aliases = new StringBuilder();
for(String keyAlias : passwords.keySet())
{
p.put(keyAlias + ".password", passwords.get(keyAlias));
if(encodedKeyData != null && encodedKeyData.get(keyAlias) != null)
{
p.put(keyAlias + ".keyData", encodedKeyData.get(keyAlias));
}
p.put(keyAlias + ".algorithm", "DESede");
aliases.append(keyAlias);
aliases.append(",");
}
if(aliases.length() > 0)
{
// remove trailing comma
aliases.delete(aliases.length() - 1, aliases.length());
}
p.put("aliases", aliases.toString());
return p;
}
};
KeyStoreParameters keyStoreParameters = new KeyStoreParameters(name, type, null, "", keyStoreLocation);
KeyStoreParameters backupKeyStoreParameters = new KeyStoreParameters(name + ".backup", type, null, "", backupKeyStoreLocation);
TestAlfrescoKeyStore keyStore = new TestAlfrescoKeyStore();
keyStore.setKeyStoreParameters(keyStoreParameters);
keyStore.setBackupKeyStoreParameters(backupKeyStoreParameters);
keyStore.setKeyResourceLoader(testKeyResourceLoader);
keyStore.setValidateKeyChanges(true);
keyStore.setEncryptionKeysRegistry(encryptionKeysRegistry);
return keyStore;
}
@Test
public void test1()
{
// missing keystore, missing backup keystore, no registered keys -> create key store with metadata key and register key
TestAlfrescoKeyStore missingMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
Collections.singletonMap(KeyProvider.ALIAS_METADATA, generateEncodedKey()), generateKeystoreName(), generateKeystoreName());
encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);
keyStoreChecker.setMainKeyStore(missingMainKeyStore);
try
{
keyStoreChecker.validateKeyStores();
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
assertTrue("", encryptionKeysRegistry.getRegisteredKeys(missingMainKeyStore.getKeyAliases()).contains(KeyProvider.ALIAS_METADATA));
assertTrue("", missingMainKeyStore.exists());
assertTrue("", missingMainKeyStore.getKey(KeyProvider.ALIAS_METADATA) != null);
}
@Test
public void test2()
{
// missing main keystore, missing backup keystore, metadata registered key -> error, re-instate the keystore
TestAlfrescoKeyStore missingMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
null, generateKeystoreName(), generateKeystoreName());
assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));
keyStoreChecker.setMainKeyStore(missingMainKeyStore);
try
{
keyStoreChecker.validateKeyStores();
fail("Should have caught missing main keystore");
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception : " + e.getMessage());
}
catch (MissingKeyException e)
{
// ok, expected
}
}
@Test
public void test3()
{
// main keystore exists, no registered metadata key -> register key
// create main keystore
TestAlfrescoKeyStore mainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
null, generateKeystoreName(), generateKeystoreName());
createAndPopulateKeyStore(mainKeyStore);
// de-register metadata key
encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);
// check keys
keyStoreChecker.setMainKeyStore(mainKeyStore);
try
{
keyStoreChecker.validateKeyStores();
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
assertTrue("", encryptionKeysRegistry.isKeyRegistered(KeyProvider.ALIAS_METADATA));
}
@Test
public void test4()
{
// create keystore, change key -> check for exception InvalidKey
// Firstly, create main keystore to register a well-known key
encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);
TestAlfrescoKeyStore keyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
null, generateKeystoreName(), generateKeystoreName());
createAndPopulateKeyStore(keyStore);
keyStoreChecker.setMainKeyStore(keyStore);
// check keys
try
{
// should register the metadata key
keyStoreChecker.validateKeyStores();
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
// check that the metadata key has been registered
assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));
// a changed main keystore with a different metadata key
// TestAlfrescoKeyStore changedMainKeyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
// null, generateKeystoreName(), generateKeystoreName());
// createAndPopulateKeyStore(changedMainKeyStore);
keyStore.changeKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));
// keyStoreChecker.setMainKeyStore(changedMainKeyStore);
try
{
keyStoreChecker.validateKeyStores();
fail("Expected key store checker to detect changed metadata key");
}
catch(InvalidKeystoreException e)
{
// ok, expected
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
}
@Test
public void test5()
{
// create main keystore, backup main keystore, change main keystore -> check that backup keystore key is ok, re-register new main key
// check that the new main keystore key has been re-registered
// Firstly, re-install main keystore to register a well-known key
encryptionKeysRegistry.unregisterKey(KeyProvider.ALIAS_METADATA);
TestAlfrescoKeyStore keyStore = getKeyStore("main", "JCEKS", Collections.singletonMap(KeyProvider.ALIAS_METADATA, "metadata"),
null, generateKeystoreName(), generateKeystoreName());
createAndPopulateKeyStore(keyStore);
try
{
keyStoreChecker.setMainKeyStore(keyStore);
keyStoreChecker.validateKeyStores();
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
keyStore.backup();
// check that the metadata key has been registered
assertTrue("", encryptionKeysRegistry.isKeyRegistered("metadata"));
// change the metadata key
keyStore.changeKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));
try
{
// should detect changed metadata key and re-register it
keyStoreChecker.validateKeyStores();
}
catch(InvalidKeystoreException e)
{
fail("Unexpected exception: " + e.getMessage());
}
catch (MissingKeyException e)
{
fail("Unexpected exception : " + e.getMessage());
}
// check that the new metadata key has been successfully re-registered by encrypting and decrypting some content with it
assertTrue("", EncryptionKeysRegistry.KEY_STATUS.OK == encryptionKeysRegistry.checkKey(KeyProvider.ALIAS_METADATA,
keyStore.getKey(KeyProvider.ALIAS_METADATA)));
}
private void createAndPopulateKeyStore(TestAlfrescoKeyStore keyStore)
{
KeyMap keyMap = new KeyMap();
keyMap.setKey(KeyProvider.ALIAS_METADATA, generateSecretKey("DESede"));
keyStore.create(keyMap, null);
}
private static class TestAlfrescoKeyStore extends AlfrescoKeyStoreImpl
{
public void create(KeyMap keys, KeyMap backupKeys)
{
this.keys = (keys != null ? keys : new KeyMap());
this.backupKeys = (backupKeys != null ? backupKeys : new KeyMap());
super.create();
}
void changeKey(String keyAlias, Key key)
{
keys.setKey(KeyProvider.ALIAS_METADATA, key);
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.encryption;
/**
*
* @since 4.0
*
*/
public class MissingKeyException extends Exception
{
private static final long serialVersionUID = -7843412242954504581L;
private String keyAlias;
public MissingKeyException(String keyAlias)
{
this.keyAlias = keyAlias;
}
public String getKeyAlias()
{
return keyAlias;
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.encryption;
/**
*
* @since 4.0
*
*/
public class MissingKeyStoreException extends Exception
{
private static final long serialVersionUID = -6484290539637378020L;
public MissingKeyStoreException(String message)
{
super(message);
}
}

View File

@@ -76,13 +76,12 @@ public class ReEncryptor implements ApplicationContextAware
private QNameDAO qnameDAO;
private MetadataEncryptor metadataEncryptor;
private KeyProvider backupKeyProvider;
private KeyProvider keyProvider;
private ApplicationContext applicationContext;
private TransactionService transactionService;
private RetryingTransactionHelper transactionHelper;
private int numThreads;
private int chunkSize;
private boolean splitTxns = true;
@@ -113,6 +112,11 @@ public class ReEncryptor implements ApplicationContextAware
{
this.jobLockService = jobLockService;
}
public void setNumThreads(int numThreads)
{
this.numThreads = numThreads;
}
public void setChunkSize(int chunkSize)
{
@@ -139,16 +143,6 @@ public class ReEncryptor implements ApplicationContextAware
this.qnameDAO = qnameDAO;
}
public void setBackupKeyProvider(KeyProvider backupKeyProvider)
{
this.backupKeyProvider = backupKeyProvider;
}
public void setKeyProvider(KeyProvider keyProvider)
{
this.keyProvider = keyProvider;
}
/**
* Attempts to get the lock. If the lock couldn't be taken, then <tt>null</tt> is returned.
*
@@ -266,7 +260,7 @@ public class ReEncryptor implements ApplicationContextAware
I18NUtil.getMessage("reencryptor.batchprocessor.name"), // TODO i18n name
transactionHelper,
provider,
2, 100,
numThreads, chunkSize,
applicationContext,
logger, 100).process(worker, splitTxns);
}
@@ -274,11 +268,11 @@ public class ReEncryptor implements ApplicationContextAware
/**
* Re-encrypt using the configured backup keystore to decrypt and the main keystore to encrypt
*/
public int bootstrapReEncrypt() throws MissingKeyStoreException
public int bootstrapReEncrypt() throws MissingKeyException
{
if(backupKeyProvider.getKey(KeyProvider.ALIAS_METADATA) == null)
if(!metadataEncryptor.backupKeyAvailable(KeyProvider.ALIAS_METADATA))
{
throw new MissingKeyStoreException("Backup key store is either not present or does not contain a metadata encryption key");
throw new MissingKeyException("Backup key store is either not present or does not contain a metadata encryption key");
}
return reEncrypt();
}
@@ -287,31 +281,30 @@ public class ReEncryptor implements ApplicationContextAware
* Re-encrypt by decrypting using the configured keystore and encrypting using a keystore configured using the provided new key store parameters.
* Called from e.g. JMX.
*
* Assumes that the main key store has been already been reloaded.
*
* Note: it is the responsibility of the end user to ensure that the underlying keystores have been set up appropriately
* i.e. the old key store is backed up to the location defined by the property '${dir.keystore}/backup-keystore' and the new
* key store replaces it. This can be done while the repository is running.
*/
public int reEncrypt() throws MissingKeyStoreException
public int reEncrypt() throws MissingKeyException
{
// refresh the key providers to pick up changes made
backupKeyProvider.refresh();
keyProvider.refresh();
if(keyProvider.getKey(KeyProvider.ALIAS_METADATA) == null)
if(!metadataEncryptor.keyAvailable(KeyProvider.ALIAS_METADATA))
{
throw new MissingKeyStoreException("Main key store is either not present or does not contain a metadata encryption key");
throw new MissingKeyException("Main key store is either not present or does not contain a metadata encryption key");
}
if(backupKeyProvider.getKey(KeyProvider.ALIAS_METADATA) == null)
if(!metadataEncryptor.backupKeyAvailable(KeyProvider.ALIAS_METADATA))
{
throw new MissingKeyStoreException("Backup key store is either not present or does not contain a metadata encryption key");
throw new MissingKeyException("Backup key store is either not present or does not contain a metadata encryption key");
}
int numProps = reEncryptImpl();
int numProps = reEncryptImpl();
return numProps;
}
protected int reEncryptImpl()
{
// Take out a re-encryptor lock
RetryingTransactionCallback<String> txnWork = new RetryingTransactionCallback<String>()
{
public String execute() throws Exception