diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java index 2a5ae200c5..0b73b5f0f5 100644 --- a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -73,6 +73,7 @@ import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.BaseSpringTest; import org.alfresco.util.GUID; +import org.alfresco.util.TestWithUserUtils; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; import org.springframework.context.ApplicationContext; @@ -1743,6 +1744,48 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest assertTrue("Serialization/deserialization failed", checkPropertyQname instanceof QName); } + public void testEncryptionAndDecryptionOfProperties() + { + QName valueToEncrypt = PROP_QNAME_CONTENT_VALUE; + QName value = PROP_QNAME_CONTENT_VALUE; + + // Test single property encryption/decryption + Serializable encryptedProperty = metadataEncryptor.encrypt(PROP_QNAME_ENCRYPTED_VALUE, valueToEncrypt); + assertTrue("Not a SealedObject", encryptedProperty instanceof SealedObject); + Serializable encryptedPropertyAgain = metadataEncryptor.encrypt(PROP_QNAME_ENCRYPTED_VALUE, encryptedProperty); + assertTrue("Re-encryption not expected", encryptedProperty == encryptedPropertyAgain); + Serializable decryptedProperty = metadataEncryptor.decrypt(PROP_QNAME_ENCRYPTED_VALUE, encryptedProperty); + assertEquals("Value not decrypted correctly", valueToEncrypt, decryptedProperty); + + // Test mass property encryption/decryption + Map properties = new HashMap(5); + properties.put(PROP_QNAME_ENCRYPTED_VALUE, valueToEncrypt); + properties.put(PROP_QNAME_QNAME_VALUE, value); + Map encryptedProperties = metadataEncryptor.encrypt(properties); + assertTrue("Not a SealedObject", encryptedProperties.get(PROP_QNAME_ENCRYPTED_VALUE) instanceof SealedObject); + assertTrue("Should not encrypt", encryptedProperties.get(PROP_QNAME_QNAME_VALUE) instanceof QName); + Map encryptedPropertiesAgain = metadataEncryptor.encrypt(encryptedProperties); + assertTrue("Map should not change", encryptedProperties == encryptedPropertiesAgain); + assertTrue( + "Re-encryption not expected", + encryptedProperties.get(PROP_QNAME_ENCRYPTED_VALUE) == encryptedPropertiesAgain.get(PROP_QNAME_ENCRYPTED_VALUE)); + assertTrue("Should not encrypt", encryptedProperties.get(PROP_QNAME_QNAME_VALUE) instanceof QName); + Map decryptedProperties = metadataEncryptor.decrypt(encryptedProperties); + assertEquals("Values not decrypted correctly", valueToEncrypt, decryptedProperties.get(PROP_QNAME_ENCRYPTED_VALUE)); + assertEquals("Values not decrypted correctly", value, decryptedProperties.get(PROP_QNAME_QNAME_VALUE)); + + // Check that nulls are handled + Map propertiesNull = new HashMap(5); + propertiesNull.put(PROP_QNAME_ENCRYPTED_VALUE, null); + propertiesNull.put(PROP_QNAME_QNAME_VALUE, null); + Map encryptedPropertiesNull = metadataEncryptor.encrypt(propertiesNull); + assertTrue("Map should not change", encryptedPropertiesNull == propertiesNull); + assertNull("Null should remain", encryptedPropertiesNull.get(PROP_QNAME_ENCRYPTED_VALUE)); + Map decryptedPropertiesNull = metadataEncryptor.decrypt(encryptedPropertiesNull); + assertTrue("Map should not change", encryptedPropertiesNull == decryptedPropertiesNull); + assertNull("Null should remain", decryptedPropertiesNull.get(PROP_QNAME_ENCRYPTED_VALUE)); + } + /** * Check that d:encrypted properties work correctly. */ @@ -1759,7 +1802,7 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest assertTrue("Properties not encrypted", checkProperty instanceof SealedObject); // create node - NodeRef nodeRef = nodeService.createNode( + final NodeRef nodeRef = nodeService.createNode( rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathA"), @@ -1774,10 +1817,46 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest checkProperty = checkProperties.get(PROP_QNAME_ENCRYPTED_VALUE); assertTrue("Encrypted property not persisted", checkProperty instanceof SealedObject); + // Decrypt individual property + checkProperty = metadataEncryptor.decrypt(PROP_QNAME_ENCRYPTED_VALUE, checkProperty); + assertEquals("Bulk property decryption failed", property, checkProperty); + + // Now decrypt en-masse + checkProperties = metadataEncryptor.decrypt(checkProperties); + checkProperty = checkProperties.get(PROP_QNAME_ENCRYPTED_VALUE); + assertEquals("Bulk property decryption failed", property, checkProperty); + // Now make sure that the value can be null - nodeService.setProperty(nodeRef, PROP_QNAME_ENCRYPTED_VALUE, null); + RetryingTransactionCallback setNullPropCallback = new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + nodeService.setProperty(nodeRef, PROP_QNAME_ENCRYPTED_VALUE, null); + return null; + } + }; + retryingTransactionHelper.doInTransaction(setNullPropCallback); // Finally, make sure that it fails if we don't encrypt + try + { + RetryingTransactionCallback setUnencryptedPropCallback = new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + nodeService.setProperty(nodeRef, PROP_QNAME_ENCRYPTED_VALUE, "No encrypted"); + return null; + } + }; + retryingTransactionHelper.doInTransaction(setUnencryptedPropCallback); + fail("Failed to detect unencrypted property"); // This behaviour may change + } + catch (RuntimeException e) + { + // Expected + } } @SuppressWarnings("unchecked") diff --git a/source/java/org/alfresco/repo/node/encryption/MetadataEncryptor.java b/source/java/org/alfresco/repo/node/encryption/MetadataEncryptor.java index 9cfb335880..f40623968e 100644 --- a/source/java/org/alfresco/repo/node/encryption/MetadataEncryptor.java +++ b/source/java/org/alfresco/repo/node/encryption/MetadataEncryptor.java @@ -2,7 +2,9 @@ package org.alfresco.repo.node.encryption; import java.io.Serializable; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import javax.crypto.SealedObject; @@ -76,11 +78,40 @@ public class MetadataEncryptor { return inbound; } + if (inbound instanceof SealedObject) + { + return inbound; + } Serializable outbound = encryptor.sealObject(KeyProvider.ALIAS_METADATA, null, inbound); // Done return outbound; } + /** + * Decrypt a property if the data definition (model-specific) requires it. + *

+ * This method can only be called by the 'system' user. + * + * @param propertyQName the property qualified name + * @param inbound the property to decrypt + * @return the decrypted property or the original if it wasn't encrypted + */ + public Serializable decrypt(QName propertyQName, Serializable inbound) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + if (inbound == null || propertyDef == null || !(propertyDef.getDataType().getName().equals(DataTypeDefinition.ENCRYPTED))) + { + return inbound; + } + if (!(inbound instanceof SealedObject)) + { + return inbound; + } + Serializable outbound = encryptor.unsealObject(KeyProvider.ALIAS_METADATA, inbound); + // Done + return outbound; + } + /** * Encrypt properties if their data definition (model-specific) requires it. * The values provided can be mixed; values will be encrypted only if required. @@ -93,36 +124,35 @@ public class MetadataEncryptor */ public Map encrypt(Map inbound) { - boolean encrypt = false; + Set encryptedProperties = new HashSet(5); for (Map.Entry entry : inbound.entrySet()) { - QName key = entry.getKey(); - PropertyDefinition propertyDef = dictionaryService.getProperty(key); + QName qname = entry.getKey(); + Serializable value = entry.getValue(); + PropertyDefinition propertyDef = dictionaryService.getProperty(qname); if (propertyDef != null && (propertyDef.getDataType().getName().equals(DataTypeDefinition.ENCRYPTED))) { - encrypt = true; - break; + if (value != null && !(value instanceof SealedObject)) + { + encryptedProperties.add(qname); + } } } - if (!encrypt) + if (encryptedProperties.isEmpty()) { // Nothing to do return inbound; } // Encrypt, in place, using a copied map Map outbound = new HashMap(inbound); - for (Map.Entry entry : inbound.entrySet()) + for (QName propertyQName : encryptedProperties) { - Serializable value = entry.getValue(); - if (value != null && (value instanceof SealedObject)) - { - // Straight copy, i.e. do nothing - continue; - } - // Have to decrypt the value + // We have already checked for nulls and conversions + Serializable value = inbound.get(propertyQName); + // Have to encrypt the value Serializable encryptedValue = encryptor.sealObject(KeyProvider.ALIAS_METADATA, null, value); // Store it back - outbound.put(entry.getKey(), encryptedValue); + outbound.put(propertyQName, encryptedValue); } // Done return outbound; @@ -142,35 +172,35 @@ public class MetadataEncryptor { checkAuthentication(); - boolean decrypt = false; + Set encryptedProperties = new HashSet(5); for (Map.Entry entry : inbound.entrySet()) { + QName qname = entry.getKey(); Serializable value = entry.getValue(); - if (value != null && (value instanceof SealedObject)) + PropertyDefinition propertyDef = dictionaryService.getProperty(qname); + if (propertyDef != null && (propertyDef.getDataType().getName().equals(DataTypeDefinition.ENCRYPTED))) { - decrypt = true; - break; + if (value != null && (value instanceof SealedObject)) + { + encryptedProperties.add(qname); + } } } - if (!decrypt) + if (encryptedProperties.isEmpty()) { // Nothing to do return inbound; } // Decrypt, in place, using a copied map Map outbound = new HashMap(inbound); - for (Map.Entry entry : inbound.entrySet()) + for (QName propertyQName : encryptedProperties) { - Serializable value = entry.getValue(); - if (value != null && (value instanceof SealedObject)) - { - // Straight copy, i.e. do nothing - continue; - } + // We have already checked for nulls and conversions + Serializable value = inbound.get(propertyQName); // Have to decrypt the value - Serializable decryptedValue = encryptor.unsealObject(KeyProvider.ALIAS_METADATA, value); + Serializable unencryptedValue = encryptor.unsealObject(KeyProvider.ALIAS_METADATA, value); // Store it back - outbound.put(entry.getKey(), decryptedValue); + outbound.put(propertyQName, unencryptedValue); } // Done return outbound; diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java index 8380fdc8dd..5eba3dc4af 100644 --- a/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java @@ -19,6 +19,8 @@ package org.alfresco.repo.node.integrity; import java.io.InputStream; +import java.io.Serializable; +import java.util.Map; import javax.transaction.UserTransaction; @@ -28,6 +30,7 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.node.BaseNodeServiceTest; +import org.alfresco.repo.node.encryption.MetadataEncryptor; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.NodeRef; @@ -97,7 +100,7 @@ public class IntegrityTest extends TestCase private ServiceRegistry serviceRegistry; private NodeService nodeService; private NodeRef rootNodeRef; - private PropertyMap allProperties; + private Map allProperties; private UserTransaction txn; private AuthenticationComponent authenticationComponent; @@ -116,6 +119,8 @@ public class IntegrityTest extends TestCase integrityChecker.setFailOnViolation(true); integrityChecker.setTraceOn(true); integrityChecker.setMaxErrorsPerTransaction(100); // we want to count the correct number of errors + + MetadataEncryptor encryptor = (MetadataEncryptor) ctx.getBean("metadataEncryptor"); serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); nodeService = serviceRegistry.getNodeService(); @@ -141,6 +146,7 @@ public class IntegrityTest extends TestCase allProperties.put(TEST_PROP_INT_B, "456"); allProperties.put(TEST_PROP_ENCRYPTED_A, "ABC"); allProperties.put(TEST_PROP_ENCRYPTED_B, "DEF"); + allProperties = encryptor.encrypt(allProperties); } public void tearDown() throws Exception @@ -166,7 +172,7 @@ public class IntegrityTest extends TestCase /** * Create a node of the given type, and hanging off the root node */ - private NodeRef createNode(String name, QName type, PropertyMap properties) + private NodeRef createNode(String name, QName type, Map properties) { return nodeService.createNode( rootNodeRef, @@ -247,8 +253,17 @@ public class IntegrityTest extends TestCase public void testCreateWithoutEncryption() throws Exception { - NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_ENCRYPTED_PROPERTIES, allProperties); - checkIntegrityExpectFailure("Failed to detect unencrypted properties", 2); + allProperties.put(TEST_PROP_ENCRYPTED_A, "Not encrypted"); + try + { + NodeRef nodeRef = createNode("abc", TEST_TYPE_WITH_ENCRYPTED_PROPERTIES, allProperties); + fail("Current detection of unencrypted properties is done by NodeService."); + } + catch (Throwable e) + { + // Expected + } + // checkIntegrityExpectFailure("Failed to detect unencrypted properties", 2); } public void testCreateWithoutPropertiesForAspect() throws Exception