diff --git a/client/pom.xml b/client/pom.xml
index 590a9ad5022..9453159a487 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -75,6 +75,11 @@
cloud-plugin-user-authenticator-md5
${project.version}
+
+ org.apache.cloudstack
+ cloud-plugin-user-authenticator-pbkdf2
+ ${project.version}
+
org.apache.cloudstack
cloud-plugin-user-authenticator-plaintext
diff --git a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
index 939cffebce9..d967540af5a 100644
--- a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
+++ b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
@@ -33,7 +33,7 @@
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
-
+
-
+
storage-allocators/random
user-authenticators/ldap
user-authenticators/md5
+ user-authenticators/pbkdf2
user-authenticators/plain-text
user-authenticators/saml2
user-authenticators/sha256salted
diff --git a/plugins/user-authenticators/pbkdf2/pom.xml b/plugins/user-authenticators/pbkdf2/pom.xml
new file mode 100644
index 00000000000..e6560456b40
--- /dev/null
+++ b/plugins/user-authenticators/pbkdf2/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+ cloud-plugin-user-authenticator-pbkdf2
+ Apache CloudStack Plugin - User Authenticator PBKDF2-SHA-256
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.5.0-SNAPSHOT
+ ../../pom.xml
+
+
diff --git a/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/module.properties b/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/module.properties
new file mode 100644
index 00000000000..7c2b38d654d
--- /dev/null
+++ b/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/module.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+name=pbkdf2
+parent=api
diff --git a/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/spring-pbkdf2-context.xml b/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/spring-pbkdf2-context.xml
new file mode 100644
index 00000000000..a6272ddb0de
--- /dev/null
+++ b/plugins/user-authenticators/pbkdf2/resources/META-INF/cloudstack/pbkdf2/spring-pbkdf2-context.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/plugins/user-authenticators/pbkdf2/src/org/apache/cloudstack/server/auth/PBKDF2UserAuthenticator.java b/plugins/user-authenticators/pbkdf2/src/org/apache/cloudstack/server/auth/PBKDF2UserAuthenticator.java
new file mode 100644
index 00000000000..43c32c7ae5f
--- /dev/null
+++ b/plugins/user-authenticators/pbkdf2/src/org/apache/cloudstack/server/auth/PBKDF2UserAuthenticator.java
@@ -0,0 +1,143 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.cloudstack.server.auth;
+
+import com.cloud.server.auth.DefaultUserAuthenticator;
+import com.cloud.server.auth.UserAuthenticator;
+import com.cloud.user.UserAccount;
+import com.cloud.user.dao.UserAccountDao;
+import com.cloud.utils.ConstantTimeComparator;
+import com.cloud.utils.Pair;
+import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.bouncycastle.crypto.PBEParametersGenerator;
+import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.bouncycastle.util.encoders.Base64;
+
+import javax.ejb.Local;
+import javax.inject.Inject;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Map;
+
+import static java.lang.String.format;
+
+@Local({UserAuthenticator.class})
+public class PBKDF2UserAuthenticator extends DefaultUserAuthenticator {
+ public static final Logger s_logger = Logger.getLogger(PBKDF2UserAuthenticator.class);
+ private static final int s_saltlen = 64;
+ private static final int s_rounds = 100000;
+ private static final int s_keylen = 512;
+
+ @Inject
+ private UserAccountDao _userAccountDao;
+
+ public Pair authenticate(String username, String password, Long domainId, Map requestParameters) {
+ if (s_logger.isDebugEnabled()) {
+ s_logger.debug("Retrieving user: " + username);
+ }
+ boolean isValidUser = false;
+ UserAccount user = this._userAccountDao.getUserAccount(username, domainId);
+ if (user != null) {
+ isValidUser = true;
+ } else {
+ s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
+ }
+
+ byte[] salt = new byte[0];
+ int rounds = s_rounds;
+ try {
+ if (isValidUser) {
+ String[] storedPassword = user.getPassword().split(":");
+ if ((storedPassword.length != 3) || (!StringUtils.isNumeric(storedPassword[2]))) {
+ s_logger.warn("The stored password for " + username + " isn't in the right format for this authenticator");
+ isValidUser = false;
+ } else {
+ // Encoding format = ::
+ salt = decode(storedPassword[0]);
+ rounds = Integer.parseInt(storedPassword[2]);
+ }
+ }
+ boolean result = false;
+ if (isValidUser && validateCredentials(password, salt)) {
+ result = ConstantTimeComparator.compareStrings(user.getPassword(), encode(password, salt, rounds));
+ }
+
+ UserAuthenticator.ActionOnFailedAuthentication action = null;
+ if ((!result) && (isValidUser)) {
+ action = UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT;
+ }
+ return new Pair(Boolean.valueOf(result), action);
+ } catch (NumberFormatException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ } catch (UnsupportedEncodingException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ } catch (InvalidKeySpecException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ }
+ }
+
+ public String encode(String password)
+ {
+ try
+ {
+ return encode(password, makeSalt(), s_rounds);
+ } catch (NoSuchAlgorithmException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ } catch (UnsupportedEncodingException e) {
+ throw new CloudRuntimeException("Unable to hash password", e);
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Exception in EncryptUtil.createKey ", e);
+ throw new CloudRuntimeException("Unable to hash password", e);
+ }
+ }
+
+ public String encode(String password, byte[] salt, int rounds)
+ throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
+ PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator();
+ generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(
+ password.toCharArray()),
+ salt,
+ rounds);
+ return format("%s:%s:%d", encode(salt),
+ encode(((KeyParameter)generator.generateDerivedParameters(s_keylen)).getKey()), rounds);
+ }
+
+ public static byte[] makeSalt() throws NoSuchAlgorithmException {
+ SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
+ byte[] salt = new byte[s_saltlen];
+ sr.nextBytes(salt);
+ return salt;
+ }
+
+ private static boolean validateCredentials(String plainPassword, byte[] hash) {
+ return !(plainPassword == null || plainPassword.isEmpty() || hash == null || hash.length == 0);
+ }
+
+ private static String encode(byte[] input) {
+ return new String(Base64.encode(input));
+ }
+
+ private static byte[] decode(String input) throws UnsupportedEncodingException {
+ return Base64.decode(input.getBytes("UTF-8"));
+ }
+}
diff --git a/plugins/user-authenticators/pbkdf2/test/org/apache/cloudstack/server/auth/PBKD2UserAuthenticatorTest.java b/plugins/user-authenticators/pbkdf2/test/org/apache/cloudstack/server/auth/PBKD2UserAuthenticatorTest.java
new file mode 100644
index 00000000000..f4014167cff
--- /dev/null
+++ b/plugins/user-authenticators/pbkdf2/test/org/apache/cloudstack/server/auth/PBKD2UserAuthenticatorTest.java
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.cloudstack.server.auth;
+
+import com.cloud.server.auth.UserAuthenticator;
+import com.cloud.user.UserAccountVO;
+import com.cloud.user.dao.UserAccountDao;
+import com.cloud.utils.Pair;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.lang.reflect.Field;
+import java.security.NoSuchAlgorithmException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PBKD2UserAuthenticatorTest {
+ @Mock
+ UserAccountDao dao;
+
+ @Test
+ public void encodePasswordTest() {
+ PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
+ String encodedPassword = authenticator.encode("password123ABCS!@#$%");
+ Assert.assertTrue(encodedPassword.length() < 255 && encodedPassword.length() >= 182);
+ }
+
+ @Test
+ public void saltTest() throws NoSuchAlgorithmException {
+ byte[] salt = new PBKDF2UserAuthenticator().makeSalt();
+ Assert.assertTrue(salt.length > 16);
+ }
+
+ @Test
+ public void authenticateValidTest() throws IllegalAccessException, NoSuchFieldException {
+ PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
+ Field daoField = PBKDF2UserAuthenticator.class.getDeclaredField("_userAccountDao");
+ daoField.setAccessible(true);
+ daoField.set(authenticator, dao);
+ UserAccountVO account = new UserAccountVO();
+ account.setPassword("FMDMdx/2QjrZniqNRAgOAC1ai/CY/C+2kmKhp3vo+98pkqhO+AR6hCyUl0bOXtkq3XWqNiSQTwbi7KTiwuWhyw==:+u8T5LzCtikCPvKnUDn6JDezf1Hg2bood/ke5Oo93pz9s1eD9k/JLsa497Z3h9QWfOQfq0zvCRmkzfXMF913vQ==:4096");
+ Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
+ Pair pair = authenticator.authenticate("admin", "password", 1l, null);
+ Assert.assertTrue(pair.first());
+ }
+
+ @Test
+ public void authenticateInValidTest() throws IllegalAccessException, NoSuchFieldException {
+ PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
+ Field daoField = PBKDF2UserAuthenticator.class.getDeclaredField("_userAccountDao");
+ daoField.setAccessible(true);
+ daoField.set(authenticator, dao);
+ UserAccountVO account = new UserAccountVO();
+ account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99");
+ Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
+ Pair pair = authenticator.authenticate("admin", "password", 1l, null);
+ Assert.assertFalse(pair.first());
+ }
+}