diff --git a/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml b/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml index 0420c93b73..7388aa2dba 100644 --- a/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml @@ -76,13 +76,34 @@ + + + + + + + + + + + + + + + + + + - - + @@ -147,17 +168,14 @@ - - + - - - + diff --git a/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java b/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java index 68245c011b..5df9c35e5b 100644 --- a/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java +++ b/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java @@ -19,11 +19,14 @@ package org.alfresco.repo.security.authentication; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.service.namespace.QName; import org.alfresco.util.ParameterCheck; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -31,41 +34,22 @@ import java.util.Map; * A configurable password encoding that delegates the encoding to a Map of * configured encoders. * - * @Author Gethin James + * @author Gethin James */ public class CompositePasswordEncoder { private static Log logger = LogFactory.getLog(CompositePasswordEncoder.class); private Map encoders; private String preferredEncoding; - private String legacyEncoding; - private String legacyEncodingProperty; - public void setLegacyEncoding(String legacyEncoding) - { - this.legacyEncoding = legacyEncoding; - } - - public void setLegacyEncodingProperty(String legacyEncodingProperty) - { - this.legacyEncodingProperty = legacyEncodingProperty; - } + public static final List SHA256 = Arrays.asList("sha256"); + public static final List MD4 = Arrays.asList("md4"); public String getPreferredEncoding() { return preferredEncoding; } - public String getLegacyEncoding() - { - return legacyEncoding; - } - - public String getLegacyEncodingProperty() - { - return legacyEncodingProperty; - } - public void setPreferredEncoding(String preferredEncoding) { this.preferredEncoding = preferredEncoding; @@ -77,13 +61,17 @@ public class CompositePasswordEncoder } /** - * Is this the preferred encoding ? - * @param encoding a String representing the encoding + * Is the preferred encoding the last encoding to be used. + * @param hashIndicator a List representing the encoding * @return true if is correct */ - public boolean isPreferredEncoding(String encoding) + public boolean lastEncodingIsPreferred(List hashIndicator) { - return preferredEncoding.equals(encoding); + if (hashIndicator!= null && hashIndicator.size() > 0 && preferredEncoding.equals(hashIndicator.get(hashIndicator.size()-1))) + { + return true; + } + return false; } /** @@ -93,8 +81,6 @@ public class CompositePasswordEncoder { PropertyCheck.mandatory(this, "encoders", encoders); PropertyCheck.mandatory(this, "preferredEncoding", preferredEncoding); - PropertyCheck.mandatory(this, "legacyEncoding", legacyEncoding); - PropertyCheck.mandatory(this, "legacyEncodingProperty", legacyEncodingProperty); } /** @@ -118,6 +104,17 @@ public class CompositePasswordEncoder return encoded; } + /** + * Encodes a password in the preferred encoding. + * @param rawPassword mandatory password + * @param salt optional salt + * @return Encoded password + */ + public String encodePreferred(String rawPassword, Object salt) + { + return encode(getPreferredEncoding(), rawPassword, salt); + } + /** * Encode a password using the specified encoderKey * @param encoderKey the encoder to use diff --git a/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticatedUser.java b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticatedUser.java new file mode 100644 index 0000000000..7e0585708f --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticatedUser.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2016 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 . + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.providers.dao.User; + +import java.io.Serializable; +import java.util.List; + +/** + * A user authenticated by the Alfresco repository using RepositoryAuthenticationDao + * @author Gethin James + */ +public class RepositoryAuthenticatedUser extends User +{ + private List hashIndicator; + private Serializable salt; + + public Serializable getSalt() + { + return salt; + } + + public List getHashIndicator() + { + return hashIndicator; + + } + + public RepositoryAuthenticatedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, GrantedAuthority[] authorities, List hashIndicator, Serializable salt) throws IllegalArgumentException + { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + this.hashIndicator = hashIndicator; + this.salt = salt; + } + +} diff --git a/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java index 6bbf267fc6..d41995bf54 100644 --- a/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java +++ b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDao.java @@ -19,6 +19,8 @@ package org.alfresco.repo.security.authentication; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -29,7 +31,6 @@ import net.sf.acegisecurity.GrantedAuthorityImpl; import net.sf.acegisecurity.UserDetails; import net.sf.acegisecurity.providers.dao.User; import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; -import net.sf.acegisecurity.providers.encoding.PasswordEncoder; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; @@ -55,6 +56,7 @@ import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.EqualsHelper; import org.alfresco.util.GUID; +import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; @@ -75,13 +77,12 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In protected NodeService nodeService; protected TenantService tenantService; protected NamespacePrefixResolver namespacePrefixResolver; - protected PasswordEncoder passwordEncoder; - protected PasswordEncoder sha256PasswordEncoder; protected PolicyComponent policyComponent; private TransactionService transactionService; + private CompositePasswordEncoder compositePasswordEncoder; - // note: cache is tenant-aware (if using TransctionalCache impl) +// note: cache is tenant-aware (if using TransctionalCache impl) private SimpleCache singletonCache; // eg. for user folder nodeRef private final String KEY_USERFOLDER_NODEREF = "key.userfolder.noderef"; @@ -117,16 +118,6 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In { this.singletonCache = singletonCache; } - - public void setPasswordEncoder(PasswordEncoder passwordEncoder) - { - this.passwordEncoder = passwordEncoder; - } - - public void setSha256PasswordEncoder(PasswordEncoder passwordEncoder) - { - this.sha256PasswordEncoder = passwordEncoder; - } public void setPolicyComponent(PolicyComponent policyComponent) { @@ -143,6 +134,11 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In this.transactionService = transactionService; } + public void setCompositePasswordEncoder(CompositePasswordEncoder compositePasswordEncoder) + { + this.compositePasswordEncoder = compositePasswordEncoder; + } + public void afterPropertiesSet() throws Exception { this.policyComponent.bindClassBehaviour( @@ -172,6 +168,15 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In { return userDetails; } + + if (userDetails instanceof RepositoryAuthenticatedUser) + { + RepositoryAuthenticatedUser repoUser = (RepositoryAuthenticatedUser) userDetails; + return new RepositoryAuthenticatedUser(userDetails.getUsername(), userDetails.getPassword(), userDetails.isEnabled(), + userDetails.isAccountNonExpired(), false, + userDetails.isAccountNonLocked(), userDetails.getAuthorities(), repoUser.getHashIndicator(), repoUser.getSalt()); + } + // If the credentials have expired, we must return a copy with the flag set return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.isEnabled(), userDetails.isAccountNonExpired(), false, @@ -246,12 +251,12 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In // Extract values from the query results NodeRef userRef = tenantService.getName(results.get(0).getChildRef()); Map properties = nodeService.getProperties(userRef); - String password = DefaultTypeConverter.INSTANCE.convert(String.class, - properties.get(ContentModel.PROP_PASSWORD)); + Pair, String> hashPassword = determinePasswordHash(properties); // Report back the user name as stored on the user String userName = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_USER_USERNAME)); + Serializable salt = properties.get(ContentModel.PROP_SALT); GrantedAuthority[] gas = new GrantedAuthority[1]; gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED"); @@ -261,14 +266,16 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In Date credentialsExpiryDate = getCredentialsExpiryDate(userName, properties, isAdminAuthority); boolean credentialsHaveNotExpired = (credentialsExpiryDate == null || credentialsExpiryDate.getTime() >= System.currentTimeMillis()); - UserDetails ud = new User( + UserDetails ud = new RepositoryAuthenticatedUser( userName, - password, + hashPassword.getSecond(), getEnabled(userName, properties, isAdminAuthority), !getHasExpired(userName, properties, isAdminAuthority), credentialsHaveNotExpired, !getLocked(userName, properties, isAdminAuthority), - gas); + gas, + hashPassword.getFirst(), + salt); cacheEntry = new CacheEntry(userRef, ud, credentialsExpiryDate); // Only cache positive results @@ -282,6 +289,78 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In return transactionService.getRetryingTransactionHelper().doInTransaction(new SearchUserNameCallback(), true); } + /** + * Should we rehash the password by updating the properties + * @param properties + * @return + */ + public boolean rehashedPassword(Map properties) + { + List hashIndicator = (List) properties.get(ContentModel.PROP_HASH_INDICATOR); + Pair, String> passwordHash = determinePasswordHash(properties); + + if (!compositePasswordEncoder.lastEncodingIsPreferred(passwordHash.getFirst())) + { + //We need to double hash + List nowHashed = new ArrayList(); + nowHashed.addAll(passwordHash.getFirst()); + nowHashed.add(compositePasswordEncoder.getPreferredEncoding()); + Object salt = properties.get(ContentModel.PROP_SALT); + properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(passwordHash.getSecond()), salt)); + properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable)nowHashed); + properties.remove(ContentModel.PROP_PASSWORD); + properties.remove(ContentModel.PROP_PASSWORD_SHA256); + return true; + } + + if (hashIndicator == null) + { + //Already the preferred encoding, just set it + properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable)passwordHash.getFirst()); + properties.put(ContentModel.PROP_PASSWORD_HASH, passwordHash.getSecond()); + properties.remove(ContentModel.PROP_PASSWORD); + properties.remove(ContentModel.PROP_PASSWORD_SHA256); + return true; + } + + return false; + } + + /** + * Where is the password and how is it encoded? + * @param properties + * @return + */ + protected Pair, String> determinePasswordHash(Map properties) + { + List hashIndicator = (List) properties.get(ContentModel.PROP_HASH_INDICATOR); + if (hashIndicator != null && hashIndicator.size()>0) + { + //We have hashed the value so get it. + return new Pair<>(hashIndicator,DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD_HASH))); + } + else + { + String passHash = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD_SHA256)); + if (passHash != null) + { + //We have a SHA256 so use it + return new Pair<>(CompositePasswordEncoder.SHA256,passHash); + } + else + { + passHash = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD)); + if (passHash != null) + { + //Use MD4 + return new Pair<>(CompositePasswordEncoder.MD4,passHash); + } + } + } + throw new AlfrescoRuntimeException("Unable to find a user password, please check your repository authentication settings." + + "(PreferredEncoding="+compositePasswordEncoder.getPreferredEncoding()+")"); + } + @Override public void createUser(String caseSensitiveUserName, char[] rawPassword) throws AuthenticationException { @@ -297,8 +376,8 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In properties.put(ContentModel.PROP_USER_USERNAME, caseSensitiveUserName); String salt = GUID.generate(); properties.put(ContentModel.PROP_SALT, salt); - properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), null)); - properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256PasswordEncoder.encodePassword(new String(rawPassword), salt)); + properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(rawPassword), salt)); + properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList(compositePasswordEncoder.getPreferredEncoding())); properties.put(ContentModel.PROP_ACCOUNT_EXPIRES, Boolean.valueOf(false)); properties.put(ContentModel.PROP_CREDENTIALS_EXPIRE, Boolean.valueOf(false)); properties.put(ContentModel.PROP_ENABLED, Boolean.valueOf(true)); @@ -363,10 +442,10 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In String salt = GUID.generate(); properties.remove(ContentModel.PROP_SALT); properties.put(ContentModel.PROP_SALT, salt); + properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(rawPassword), salt)); + properties.put(ContentModel.PROP_HASH_INDICATOR, compositePasswordEncoder.getPreferredEncoding()); properties.remove(ContentModel.PROP_PASSWORD); - properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), null)); properties.remove(ContentModel.PROP_PASSWORD_SHA256); - properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256PasswordEncoder.encodePassword(new String(rawPassword), salt)); nodeService.setProperties(userRef, properties); } diff --git a/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationProvider.java b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationProvider.java new file mode 100644 index 0000000000..740d579841 --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/RepositoryAuthenticationProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2016 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 . + */ +package org.alfresco.repo.security.authentication; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider; +import net.sf.acegisecurity.providers.dao.SaltSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A DaoAuthenticationProvider that makes use of a CompositePasswordEncoder to check the + * password is correct. + * + * @author Gethin James + */ +public class RepositoryAuthenticationProvider extends DaoAuthenticationProvider +{ + private static Log logger = LogFactory.getLog(RepositoryAuthenticationProvider.class); + CompositePasswordEncoder compositePasswordEncoder; + + public void setCompositePasswordEncoder(CompositePasswordEncoder compositePasswordEncoder) + { + this.compositePasswordEncoder = compositePasswordEncoder; + } + + @Override + protected boolean isPasswordCorrect(Authentication authentication, UserDetails user) + { + if (user instanceof RepositoryAuthenticatedUser) + { + RepositoryAuthenticatedUser repoUser = (RepositoryAuthenticatedUser) user; + return compositePasswordEncoder.matchesPassword(authentication.getCredentials().toString(),user.getPassword(), repoUser.getSalt(), repoUser.getHashIndicator() ); + } + + logger.error("Password check error for "+user.getUsername()+" unknown user type: "+user.getClass().getName()); + return false; + } +} diff --git a/source/test-java/org/alfresco/AllUnitTestsSuite.java b/source/test-java/org/alfresco/AllUnitTestsSuite.java index 7a6453ebde..b3f328da08 100644 --- a/source/test-java/org/alfresco/AllUnitTestsSuite.java +++ b/source/test-java/org/alfresco/AllUnitTestsSuite.java @@ -101,6 +101,8 @@ public class AllUnitTestsSuite extends TestSuite suite.addTest(new JUnit4TestAdapter(org.alfresco.util.BeanExtenderUnitTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class)); + suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class)); + suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.security.authentication.RepositoryAuthenticationDaoHashingTest.class)); suite.addTest(org.alfresco.traitextender.TraitExtenderUnitTestSuite.suite()); suite.addTest(org.alfresco.repo.virtual.VirtualizationUnitTestSuite.suite()); } diff --git a/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java b/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java index dc27b2c186..bd1ffdfa9c 100644 --- a/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java +++ b/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java @@ -89,8 +89,7 @@ public class AuthenticationTest extends TestCase private AuthorityService authorityService; private TenantService tenantService; private TenantAdminService tenantAdminService; - private MD4PasswordEncoder passwordEncoder; - private PasswordEncoder sha256PasswordEncoder; + private CompositePasswordEncoder compositePasswordEncoder; private MutableAuthenticationDao dao; private AuthenticationManager authenticationManager; private TicketComponent ticketComponent; @@ -148,8 +147,7 @@ public class AuthenticationTest extends TestCase authorityService = (AuthorityService) ctx.getBean("authorityService"); tenantService = (TenantService) ctx.getBean("tenantService"); tenantAdminService = (TenantAdminService) ctx.getBean("tenantAdminService"); - passwordEncoder = (MD4PasswordEncoder) ctx.getBean("passwordEncoder"); - sha256PasswordEncoder = (PasswordEncoder) ctx.getBean("sha256PasswordEncoder"); + compositePasswordEncoder = (CompositePasswordEncoder) ctx.getBean("compositePasswordEncoder"); ticketComponent = (TicketComponent) ctx.getBean("ticketComponent"); authenticationService = (MutableAuthenticationService) ctx.getBean("authenticationService"); pubAuthenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService"); @@ -228,8 +226,7 @@ public class AuthenticationTest extends TestCase dao.setTenantService(tenantService); dao.setNodeService(nodeService); dao.setNamespaceService(getNamespacePrefixReolsver("")); - dao.setPasswordEncoder(passwordEncoder); - dao.setSha256PasswordEncoder(sha256PasswordEncoder); + dao.setCompositePasswordEncoder(compositePasswordEncoder); dao.setPolicyComponent(policyComponent); dao.setAuthenticationCache(authenticationCache); dao.setSingletonCache(immutableSingletonCache); @@ -449,8 +446,7 @@ public class AuthenticationTest extends TestCase dao.setNodeService(nodeService); dao.setAuthorityService(authorityService); dao.setNamespaceService(getNamespacePrefixReolsver("")); - dao.setPasswordEncoder(passwordEncoder); - dao.setSha256PasswordEncoder(sha256PasswordEncoder); + dao.setCompositePasswordEncoder(compositePasswordEncoder); dao.setPolicyComponent(policyComponent); dao.setAuthenticationCache(authenticationCache); dao.setSingletonCache(immutableSingletonCache); @@ -495,10 +491,6 @@ public class AuthenticationTest extends TestCase dao.createUser("Andy", "cabbage".toCharArray()); assertNotNull(dao.getUserOrNull("Andy")); - byte[] decodedHash = passwordEncoder.decodeHash(dao.getMD4HashedPassword("Andy")); - byte[] testHash = MessageDigest.getInstance("MD4").digest("cabbage".getBytes("UnicodeLittleUnmarked")); - assertEquals(new String(decodedHash), new String(testHash)); - UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy"); assertNotNull(AndyDetails); assertEquals("Andy", AndyDetails.getUsername()); @@ -508,7 +500,7 @@ public class AuthenticationTest extends TestCase assertTrue(AndyDetails.isCredentialsNonExpired()); assertTrue(AndyDetails.isEnabled()); assertNotSame("cabbage", AndyDetails.getPassword()); - assertEquals(AndyDetails.getPassword(), passwordEncoder.encodePassword("cabbage", dao.getSalt(AndyDetails))); + assertEquals(AndyDetails.getPassword(), compositePasswordEncoder.encodePreferred("cabbage", null)); assertEquals(1, AndyDetails.getAuthorities().length); // Object oldSalt = dao.getSalt(AndyDetails); diff --git a/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java b/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java index 3e93a21dce..2237741c64 100644 --- a/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java +++ b/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java @@ -28,11 +28,11 @@ import static org.junit.Assert.fail; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.util.GUID; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,20 +43,17 @@ import java.util.Map; */ public class CompositePasswordEncoderTest { - CompositePasswordEncoder encoder; - static Map encodersConfig; + public static Map encodersConfig; private static final String SOURCE_PASSWORD = "SOURCE PASS Word $%%^ #';/,_+{+} like €this"+"\u4eca\u65e5\u306f\u4e16\u754c"; - @BeforeClass - public static void setUpClass() throws Exception - { + static { encodersConfig = new HashMap<>(); encodersConfig.put("md4", new MD4PasswordEncoderImpl()); encodersConfig.put("sha256", new ShaPasswordEncoderImpl(256)); encodersConfig.put("bcrypt10",new BCryptPasswordEncoder(10)); - encodersConfig.put("bcrypt20",new BCryptPasswordEncoder(20)); - encodersConfig.put("bcrypt30",new BCryptPasswordEncoder(30)); + encodersConfig.put("bcrypt11",new BCryptPasswordEncoder(11)); + encodersConfig.put("bcrypt12",new BCryptPasswordEncoder(11)); encodersConfig.put("badencoder",new Object()); } @@ -251,6 +248,14 @@ public class CompositePasswordEncoderTest assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); } + @Test + public void testEncodePreferred() throws Exception + { + encoder.setPreferredEncoding("bcrypt10"); + String encoded = encoder.encodePreferred(SOURCE_PASSWORD, null); + assertTrue(encoder.matches("bcrypt10", SOURCE_PASSWORD, encoded, null)); + } + @Test public void testMandatoryProperties() throws Exception { @@ -264,8 +269,6 @@ public class CompositePasswordEncoderTest } subject.setEncoders(encodersConfig); - subject.setLegacyEncoding("md345"); - subject.setLegacyEncodingProperty("password"); try { @@ -280,8 +283,6 @@ public class CompositePasswordEncoderTest subject.init(); assertEquals("nice_encoding", subject.getPreferredEncoding()); - assertEquals("md345", subject.getLegacyEncoding()); - assertEquals("password", subject.getLegacyEncodingProperty()); } @Test @@ -289,7 +290,15 @@ public class CompositePasswordEncoderTest { CompositePasswordEncoder subject = new CompositePasswordEncoder(); subject.setPreferredEncoding("fish"); - assertTrue(subject.isPreferredEncoding("fish")); + assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("fish"))); assertEquals("fish", subject.getPreferredEncoding()); + + assertFalse(subject.lastEncodingIsPreferred((List)null)); + assertFalse(subject.lastEncodingIsPreferred(Collections.emptyList())); + assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("fish"))); + assertFalse(subject.lastEncodingIsPreferred(Arrays.asList("bird"))); + assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("bird", "fish"))); + assertFalse(subject.lastEncodingIsPreferred(Arrays.asList("bird", "fish", "dog", "cat"))); + assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("bird", "dog", "cat","fish"))); } } \ No newline at end of file diff --git a/source/test-java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDaoHashingTest.java b/source/test-java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDaoHashingTest.java new file mode 100644 index 0000000000..efde2bde2f --- /dev/null +++ b/source/test-java/org/alfresco/repo/security/authentication/RepositoryAuthenticationDaoHashingTest.java @@ -0,0 +1,177 @@ +package org.alfresco.repo.security.authentication; + +import static org.junit.Assert.*; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.junit.Before; +import org.junit.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by gethin on 01/10/15. + */ +public class RepositoryAuthenticationDaoHashingTest +{ + RepositoryAuthenticationDao authenticationDao; + CompositePasswordEncoder cpe; + + @Before + public void setUp() throws Exception + { + authenticationDao = new RepositoryAuthenticationDao(); + cpe = new CompositePasswordEncoder(); + cpe.setEncoders(CompositePasswordEncoderTest.encodersConfig); + authenticationDao.setCompositePasswordEncoder(cpe); + } + + @Test + public void testRehashedPassword() throws Exception + { + //Use md4 + cpe.setPreferredEncoding("md4"); + String salt = GUID.generate(); + String md4Hashed = cpe.encode("md4","HASHED_MY_PASSWORD", null); + String sha256Hashed = cpe.encode("sha256","HASHED_MY_PASSWORD", salt); + + Map properties = new HashMap<>(); + + properties.put(ContentModel.PROP_PASSWORD, "nonsense"); + assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + //No hashing to do but we need to update the Indicator + assertTrue(authenticationDao.rehashedPassword(properties)); + assertEquals(CompositePasswordEncoder.MD4, properties.get(ContentModel.PROP_HASH_INDICATOR)); + assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256)); + assertEquals("nonsense", properties.get(ContentModel.PROP_PASSWORD_HASH)); + //We copied the plain text (above) but it won't work (see next) + + properties.clear(); + properties.put(ContentModel.PROP_PASSWORD, "PLAIN TEXT PASSWORD"); + //We don't support plain text. + assertTrue(authenticationDao.rehashedPassword(properties)); + assertEquals(CompositePasswordEncoder.MD4, properties.get(ContentModel.PROP_HASH_INDICATOR)); + assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256)); + assertEquals("PLAIN TEXT PASSWORD", properties.get(ContentModel.PROP_PASSWORD_HASH)); + assertFalse("We copied a plain text password to the new property but" + +" the legacy encoding is set to MD4 so the password would NEVER match.", + matches("PLAIN TEXT PASSWORD", properties, cpe)); + + properties.clear(); + properties.put(ContentModel.PROP_PASSWORD, md4Hashed); + cpe.setPreferredEncoding("bcrypt10"); + + assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + //We rehashed this password by taking the md4 and hashing it by bcrypt + assertTrue(authenticationDao.rehashedPassword(properties)); + assertEquals(Arrays.asList("md4","bcrypt10"), properties.get(ContentModel.PROP_HASH_INDICATOR)); + assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + assertTrue(matches("HASHED_MY_PASSWORD", properties, cpe)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256)); + + properties.clear(); + properties.put(ContentModel.PROP_PASSWORD, "This should be ignored"); + properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256Hashed); + properties.put(ContentModel.PROP_SALT, salt); + + assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256)); + assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + //We rehashed this password by taking the sha256 and hashing it by bcrypt + assertTrue(authenticationDao.rehashedPassword(properties)); + assertEquals(Arrays.asList("sha256","bcrypt10"), properties.get(ContentModel.PROP_HASH_INDICATOR)); + assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH)); + assertTrue(matches("HASHED_MY_PASSWORD", properties, cpe)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD)); + assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256)); + } + @Test + public void testRehashedPasswordBcrypt() throws Exception + { + cpe.setPreferredEncoding("bcrypt10"); + Map properties = new HashMap<>(); + properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList("bcrypt10")); + properties.put(ContentModel.PROP_PASSWORD_HASH, "long hash"); + //Nothing to do. + assertFalse(authenticationDao.rehashedPassword(properties)); + + cpe.setPreferredEncoding("bcrypt11"); + assertTrue(authenticationDao.rehashedPassword(properties)); + assertEquals(Arrays.asList("bcrypt10","bcrypt11"),authenticationDao.determinePasswordHash(properties).getFirst()); + + cpe.setPreferredEncoding("bcrypt12"); + assertTrue(authenticationDao.rehashedPassword(properties)); + //Triple hashing! + assertEquals(Arrays.asList("bcrypt10","bcrypt11","bcrypt12"),authenticationDao.determinePasswordHash(properties).getFirst()); + } + + @Test + public void testGetPasswordHash() throws Exception + { + + Map properties = new HashMap<>(); + cpe.setPreferredEncoding("bcrypt10"); + + try + { + authenticationDao.determinePasswordHash(properties); + fail("Should throw exception"); + } + catch (AlfrescoRuntimeException are) + { + assertTrue(are.getMessage().contains("Unable to find a user password")); + } + + //if the PROP_PASSWORD field is the only one availble then we are using MD4 + properties.put(ContentModel.PROP_PASSWORD, "mypassword"); + Pair, String> passwordHashed = authenticationDao.determinePasswordHash(properties); + assertEquals(CompositePasswordEncoder.MD4, passwordHashed.getFirst()); + assertEquals("mypassword", passwordHashed.getSecond()); + + //if the PROP_PASSWORD_SHA256 field is used then we are using SHA256 + properties.put(ContentModel.PROP_PASSWORD_SHA256, "sha_password"); + passwordHashed = authenticationDao.determinePasswordHash(properties); + assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst()); + assertEquals("sha_password", passwordHashed.getSecond()); + + properties.put(ContentModel.PROP_HASH_INDICATOR, null); + //If the indicator is NULL then it still uses the old password field + passwordHashed = authenticationDao.determinePasswordHash(properties); + assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst()); + assertEquals("sha_password", passwordHashed.getSecond()); + + properties.put(ContentModel.PROP_HASH_INDICATOR, new ArrayList(0)); + //If the indicator doesn't have a value + passwordHashed = authenticationDao.determinePasswordHash(properties); + assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst()); + assertEquals("sha_password", passwordHashed.getSecond()); + + //Now it uses the correct property + properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList("myencoding")); + properties.put(ContentModel.PROP_PASSWORD_HASH, "hashed this time"); + passwordHashed = authenticationDao.determinePasswordHash(properties); + assertEquals(Arrays.asList("myencoding"), passwordHashed.getFirst()); + assertEquals("hashed this time",passwordHashed.getSecond()); + + } + + private static boolean matches(String password, Map properties, CompositePasswordEncoder cpe) + { + return cpe.matchesPassword(password, (String) properties.get(ContentModel.PROP_PASSWORD_HASH), (String) properties.get(ContentModel.PROP_SALT), (List) properties.get(ContentModel.PROP_HASH_INDICATOR) ); + } + +} \ No newline at end of file