diff --git a/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java b/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java new file mode 100644 index 0000000000..68245c011b --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/CompositePasswordEncoder.java @@ -0,0 +1,214 @@ +/* + * 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 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 java.util.List; +import java.util.Map; + +/** + * A configurable password encoding that delegates the encoding to a Map of + * configured encoders. + * + * @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 String getPreferredEncoding() + { + return preferredEncoding; + } + + public String getLegacyEncoding() + { + return legacyEncoding; + } + + public String getLegacyEncodingProperty() + { + return legacyEncodingProperty; + } + + public void setPreferredEncoding(String preferredEncoding) + { + this.preferredEncoding = preferredEncoding; + } + + public void setEncoders(Map encoders) + { + this.encoders = encoders; + } + + /** + * Is this the preferred encoding ? + * @param encoding a String representing the encoding + * @return true if is correct + */ + public boolean isPreferredEncoding(String encoding) + { + return preferredEncoding.equals(encoding); + } + + /** + * Basic init method for checking mandatory properties + */ + public void init() + { + PropertyCheck.mandatory(this, "encoders", encoders); + PropertyCheck.mandatory(this, "preferredEncoding", preferredEncoding); + PropertyCheck.mandatory(this, "legacyEncoding", legacyEncoding); + PropertyCheck.mandatory(this, "legacyEncodingProperty", legacyEncodingProperty); + } + + /** + * Encode a password + * @param rawPassword mandatory password + * @param salt optional salt + * @param encodingChain mandatory encoding chain + * @return the encoded password + */ + public String encodePassword(String rawPassword, Object salt, List encodingChain) { + + ParameterCheck.mandatoryString("rawPassword", rawPassword); + ParameterCheck.mandatoryCollection("encodingChain", encodingChain); + String encoded = new String(rawPassword); + for (String encoderKey : encodingChain) + { + encoded = encode(encoderKey, encoded, salt); + + } + if (encoded == rawPassword) throw new AlfrescoRuntimeException("No password encoding specified. "+encodingChain); + return encoded; + } + + /** + * Encode a password using the specified encoderKey + * @param encoderKey the encoder to use + * @param rawPassword mandatory password + * @param salt optional salt + * @return the encoded password + */ + protected String encode(String encoderKey, String rawPassword, Object salt) + { + ParameterCheck.mandatoryString("rawPassword", rawPassword); + ParameterCheck.mandatoryString("encoderKey", encoderKey); + Object encoder = encoders.get(encoderKey); + if (encoder == null) throw new AlfrescoRuntimeException("Invalid encoder specified: "+encoderKey); + if (encoder instanceof net.sf.acegisecurity.providers.encoding.PasswordEncoder) + { + net.sf.acegisecurity.providers.encoding.PasswordEncoder pEncoder = (net.sf.acegisecurity.providers.encoding.PasswordEncoder) encoder; + if (logger.isDebugEnabled()) { + logger.debug("Encoding using acegis PasswordEncoder: "+encoderKey); + } + return pEncoder.encodePassword(rawPassword, salt); + } + if (encoder instanceof org.springframework.security.crypto.password.PasswordEncoder) + { + org.springframework.security.crypto.password.PasswordEncoder passEncoder = (org.springframework.security.crypto.password.PasswordEncoder) encoder; + if (logger.isDebugEnabled()) { + logger.debug("Encoding using spring PasswordEncoder: "+encoderKey); + } + return passEncoder.encode(rawPassword); + } + + throw new AlfrescoRuntimeException("Unsupported encoder specified: "+encoderKey); + } + + /** + * Does the password match? + * @param rawPassword mandatory password + * @param encodedPassword mandatory hashed version + * @param salt optional salt + * @param encodingChain mandatory encoding chain + * @return true if they match + */ + public boolean matchesPassword(String rawPassword, String encodedPassword, Object salt, List encodingChain) + { + ParameterCheck.mandatoryString("rawPassword", rawPassword); + ParameterCheck.mandatoryString("encodedPassword", encodedPassword); + ParameterCheck.mandatoryCollection("encodingChain", encodingChain); + if (encodingChain.size() > 1) + { + String lastEncoder = encodingChain.get(encodingChain.size() - 1); + String encoded = encodePassword(rawPassword,salt, encodingChain.subList(0,encodingChain.size()-1)); + return matches(lastEncoder,encoded,encodedPassword,salt); + } + + if (encodingChain.size() == 1) + { + return matches(encodingChain.get(0), rawPassword, encodedPassword, salt); + } + return false; + } + + /** + * Does the password match? + * @param encoderKey the encoder to use + * @param rawPassword mandatory password + * @param encodedPassword mandatory hashed version + * @param salt optional salt + * @return true if they match + */ + protected boolean matches(String encoderKey, String rawPassword, String encodedPassword, Object salt) + { + ParameterCheck.mandatoryString("rawPassword", rawPassword); + ParameterCheck.mandatoryString("encodedPassword", encodedPassword); + ParameterCheck.mandatoryString("encoderKey", encoderKey); + Object encoder = encoders.get(encoderKey); + if (encoder == null) throw new AlfrescoRuntimeException("Invalid matches encoder specified: "+encoderKey); + if (encoder instanceof net.sf.acegisecurity.providers.encoding.PasswordEncoder) + { + net.sf.acegisecurity.providers.encoding.PasswordEncoder pEncoder = (net.sf.acegisecurity.providers.encoding.PasswordEncoder) encoder; + if (logger.isDebugEnabled()) { + logger.debug("Matching using acegis PasswordEncoder: "+encoderKey); + } + return pEncoder.isPasswordValid(encodedPassword, rawPassword, salt); + } + if (encoder instanceof org.springframework.security.crypto.password.PasswordEncoder) + { + org.springframework.security.crypto.password.PasswordEncoder passEncoder = (org.springframework.security.crypto.password.PasswordEncoder) encoder; + if (logger.isDebugEnabled()) { + logger.debug("Matching using spring PasswordEncoder: "+encoderKey); + } + return passEncoder.matches(rawPassword, encodedPassword); + } + throw new AlfrescoRuntimeException("Unsupported encoder for matching: "+encoderKey); + } +} \ No newline at end of file diff --git a/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java b/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java new file mode 100644 index 0000000000..3e93a21dce --- /dev/null +++ b/source/test-java/org/alfresco/repo/security/authentication/CompositePasswordEncoderTest.java @@ -0,0 +1,295 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +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.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests CompositePasswordEncoder + * @author Gethin James + */ +public class CompositePasswordEncoderTest +{ + + CompositePasswordEncoder encoder; + 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 + { + 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("badencoder",new Object()); + } + + @Before + public void setUp() throws Exception + { + encoder = new CompositePasswordEncoder(); + encoder.setEncoders(encodersConfig); + } + + @Test + public void testInvalidParamsEncode() throws Exception + { + String encoded = null; + try + { + encoded = encoder.encode(null, null, null); + fail("Should throw exception"); + } catch (IllegalArgumentException are) + { + assertTrue(are.getMessage().contains("rawPassword is a mandatory")); + } + + try + { + encoded = encoder.encode(null, "fred", null); + fail("Should throw exception"); + } catch (IllegalArgumentException are) + { + assertTrue(are.getMessage().contains("encoderKey is a mandatory parameter")); + } + + try + { + encoded = encoder.encode("nonsense", "fred", null); + fail("Should throw exception"); + } catch (AlfrescoRuntimeException are) + { + assertTrue(are.getMessage().contains("Invalid encoder specified")); + assertTrue(are.getMessage().endsWith("nonsense")); + } + + try + { + encoded = encoder.encode("badencoder", "fred", null); + fail("Should throw exception"); + } catch (AlfrescoRuntimeException are) + { + assertTrue(are.getMessage().contains("Unsupported encoder specified")); + assertTrue(are.getMessage().endsWith("badencoder")); + } + + } + + @Test + public void testInvalidParamsMatches() throws Exception + { + boolean match = false; + try + { + match = encoder.matches(null, null, null,null); + fail("Should throw exception"); + } catch (IllegalArgumentException are) + { + assertTrue(are.getMessage().contains("rawPassword is a mandatory")); + } + + try + { + match = encoder.matches(null, "fred", null,null); + fail("Should throw exception"); + } catch (IllegalArgumentException are) + { + assertTrue(are.getMessage().contains("encodedPassword is a mandatory parameter")); + } + + try + { + match = encoder.matches(null, "fred", "xyz",null); + fail("Should throw exception"); + } catch (IllegalArgumentException are) + { + assertTrue(are.getMessage().contains("encoderKey is a mandatory parameter")); + } + + try + { + match = encoder.matches("nonsense", "fred", "xyz",null); + fail("Should throw exception"); + } catch (AlfrescoRuntimeException are) + { + assertTrue(are.getMessage().contains("Invalid matches encoder specified")); + assertTrue(are.getMessage().endsWith("nonsense")); + } + + + try + { + match = encoder.matches("badencoder", "fred", "xyz", null); + fail("Should throw exception"); + } catch (AlfrescoRuntimeException are) + { + assertTrue(are.getMessage().contains("Unsupported encoder for matching")); + assertTrue(are.getMessage().endsWith("badencoder")); + } + } + + @Test + public void testEncodeMD4() throws Exception + { + String salt = GUID.generate(); + MD4PasswordEncoderImpl md4 = new MD4PasswordEncoderImpl(); + String sourceEncoded = md4.encodePassword(SOURCE_PASSWORD, salt); + String sourceEncodedSaltFree = md4.encodePassword(SOURCE_PASSWORD, null); + + String encoded = encoder.encode("md4", SOURCE_PASSWORD, salt); + assertEquals(sourceEncoded, encoded); + assertTrue(encoder.matches("md4", SOURCE_PASSWORD, encoded, salt)); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, Arrays.asList("md4"))); + assertEquals(sourceEncoded, encoder.encodePassword(SOURCE_PASSWORD, salt, Arrays.asList("md4"))); + + encoded = encoder.encode("md4", SOURCE_PASSWORD, null); + assertEquals(sourceEncodedSaltFree, encoded); + assertTrue(encoder.matches("md4", SOURCE_PASSWORD, sourceEncodedSaltFree, null)); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, sourceEncodedSaltFree, null, Arrays.asList("md4"))); + assertEquals(sourceEncodedSaltFree, encoder.encodePassword(SOURCE_PASSWORD, null, Arrays.asList("md4"))); + + encoded = encoder.encode("sha256", SOURCE_PASSWORD, null); + assertNotEquals(sourceEncodedSaltFree, encoded); + } + + + @Test + public void testEncodeSha256() throws Exception + { + String salt = GUID.generate(); + ShaPasswordEncoderImpl sha = new ShaPasswordEncoderImpl(256); + String sourceEncoded = sha.encodePassword(SOURCE_PASSWORD, salt); + String sourceEncodedSaltFree = sha.encodePassword(SOURCE_PASSWORD, null); + + String encoded = encoder.encode("sha256", SOURCE_PASSWORD, salt); + assertEquals(sourceEncoded, encoded); + assertTrue(encoder.matches("sha256", SOURCE_PASSWORD, encoded, salt)); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, Arrays.asList("sha256"))); + assertEquals(sourceEncoded, encoder.encodePassword(SOURCE_PASSWORD, salt, Arrays.asList("sha256"))); + + encoded = encoder.encode("sha256", SOURCE_PASSWORD, null); + assertEquals(sourceEncodedSaltFree, encoded); + assertTrue(encoder.matches("sha256", SOURCE_PASSWORD, sourceEncodedSaltFree, null)); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, sourceEncodedSaltFree, null, Arrays.asList("sha256"))); + assertEquals(sourceEncodedSaltFree, encoder.encodePassword(SOURCE_PASSWORD, null, Arrays.asList("sha256"))); + + encoded = encoder.encode("md4", SOURCE_PASSWORD, null); + assertNotEquals(sourceEncodedSaltFree, encoded); + } + + @Test + public void testEncodeBcrypt() throws Exception + { + String encoded = encoder.encode("bcrypt10", SOURCE_PASSWORD, null); + assertTrue(encoder.matches("bcrypt10", SOURCE_PASSWORD, encoded, null)); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, null, Arrays.asList("bcrypt10"))); + assertFalse(encoder.matches("sha256", SOURCE_PASSWORD, encoded, null)); + } + + @Test + public void testEncodeChain() throws Exception + { + String salt = GUID.generate(); + List mdbChain = Arrays.asList("bcrypt10"); + String encoded = encoder.encodePassword(SOURCE_PASSWORD, null, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, null, mdbChain)); + + mdbChain = Arrays.asList("md4","bcrypt10"); + encoded = encoder.encodePassword(SOURCE_PASSWORD, salt, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); + + mdbChain = Arrays.asList("sha256","bcrypt10"); + encoded = encoder.encodePassword(SOURCE_PASSWORD, salt, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); + + mdbChain = Arrays.asList("md4","sha256","bcrypt10"); + encoded = encoder.encodePassword(SOURCE_PASSWORD, salt, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); + + mdbChain = Arrays.asList("md4","sha256"); + encoded = encoder.encodePassword(SOURCE_PASSWORD, salt, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); + + mdbChain = Arrays.asList("md4","sha256","md4","sha256","bcrypt10"); + encoded = encoder.encodePassword(SOURCE_PASSWORD, salt, mdbChain); + assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); + } + + @Test + public void testMandatoryProperties() throws Exception + { + CompositePasswordEncoder subject = new CompositePasswordEncoder(); + try + { + subject.init(); + } catch (AlfrescoRuntimeException expected) + { + expected.getMessage().contains("property_not_set"); + } + + subject.setEncoders(encodersConfig); + subject.setLegacyEncoding("md345"); + subject.setLegacyEncodingProperty("password"); + + try + { + subject.init(); + } catch (AlfrescoRuntimeException expected) + { + expected.getMessage().contains("property_not_set"); + } + + //No default preferred encoding + subject.setPreferredEncoding("nice_encoding"); + subject.init(); + + assertEquals("nice_encoding", subject.getPreferredEncoding()); + assertEquals("md345", subject.getLegacyEncoding()); + assertEquals("password", subject.getLegacyEncodingProperty()); + } + + @Test + public void testIsPreferredEncoding() throws Exception + { + CompositePasswordEncoder subject = new CompositePasswordEncoder(); + subject.setPreferredEncoding("fish"); + assertTrue(subject.isPreferredEncoding("fish")); + assertEquals("fish", subject.getPreferredEncoding()); + } +} \ No newline at end of file