diff --git a/client/pom.xml b/client/pom.xml index c3a048604b1..6e13cc7ada9 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -40,6 +40,11 @@ cloud-plugin-user-authenticator-plaintext ${project.version} + + org.apache.cloudstack + cloud-plugin-user-authenticator-sha256salted + ${project.version} + org.apache.cloudstack cloud-plugin-network-nvp diff --git a/client/tomcatconf/components.xml.in b/client/tomcatconf/components.xml.in index 2953eb781a6..5957b61c0fb 100755 --- a/client/tomcatconf/components.xml.in +++ b/client/tomcatconf/components.xml.in @@ -109,6 +109,7 @@ under the License. + diff --git a/plugins/pom.xml b/plugins/pom.xml index dbdea24e17b..2009302423e 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -45,6 +45,7 @@ user-authenticators/ldap user-authenticators/md5 user-authenticators/plain-text + user-authenticators/sha256salted diff --git a/plugins/user-authenticators/ldap/src/com/cloud/server/auth/LDAPUserAuthenticator.java b/plugins/user-authenticators/ldap/src/com/cloud/server/auth/LDAPUserAuthenticator.java index 7c6e52f6659..43874f61cb7 100644 --- a/plugins/user-authenticators/ldap/src/com/cloud/server/auth/LDAPUserAuthenticator.java +++ b/plugins/user-authenticators/ldap/src/com/cloud/server/auth/LDAPUserAuthenticator.java @@ -15,6 +15,8 @@ // package com.cloud.server.auth; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; @@ -31,6 +33,7 @@ import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import org.apache.log4j.Logger; +import org.bouncycastle.util.encoders.Base64; import com.cloud.api.ApiConstants.LDAPParams; import com.cloud.configuration.Config; @@ -40,6 +43,7 @@ import com.cloud.user.UserAccount; import com.cloud.user.dao.UserAccountDao; import com.cloud.utils.component.ComponentLocator; import com.cloud.utils.crypt.DBEncryptionUtil; +import com.cloud.utils.exception.CloudRuntimeException; @Local(value={UserAuthenticator.class}) @@ -159,4 +163,17 @@ public class LDAPUserAuthenticator extends DefaultUserAuthenticator { _userAccountDao = locator.getDao(UserAccountDao.class); return true; } + + @Override + public String encode(String password) { + // Password is not used, so set to a random string + try { + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + byte bytes[] = new byte[20]; + randomGen.nextBytes(bytes); + return Base64.encode(bytes).toString(); + } catch (NoSuchAlgorithmException e) { + throw new CloudRuntimeException("Failed to generate random password",e); + } + } } diff --git a/plugins/user-authenticators/md5/src/com/cloud/server/auth/MD5UserAuthenticator.java b/plugins/user-authenticators/md5/src/com/cloud/server/auth/MD5UserAuthenticator.java index f4b6f021f3b..b0cf0b03cd6 100644 --- a/plugins/user-authenticators/md5/src/com/cloud/server/auth/MD5UserAuthenticator.java +++ b/plugins/user-authenticators/md5/src/com/cloud/server/auth/MD5UserAuthenticator.java @@ -15,6 +15,9 @@ package com.cloud.server.auth; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Map; import javax.ejb.Local; @@ -26,6 +29,7 @@ import com.cloud.server.ManagementServer; import com.cloud.user.UserAccount; import com.cloud.user.dao.UserAccountDao; import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; /** * Simple UserAuthenticator that performs a MD5 hash of the password before @@ -49,31 +53,7 @@ public class MD5UserAuthenticator extends DefaultUserAuthenticator { return false; } - /** - MessageDigest md5; - try { - md5 = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new CloudRuntimeException("Error", e); - } - md5.reset(); - BigInteger pwInt = new BigInteger(1, md5.digest(password.getBytes())); - - // make sure our MD5 hash value is 32 digits long... - StringBuffer sb = new StringBuffer(); - String pwStr = pwInt.toString(16); - int padding = 32 - pwStr.length(); - for (int i = 0; i < padding; i++) { - sb.append('0'); - } - sb.append(pwStr); - **/ - - // Will: The MD5Authenticator is now a straight pass-through comparison of the - // the passwords because we will not assume that the password passed in has - // already been MD5 hashed. I am keeping the above code in case this requirement changes - // or people need examples of how to MD5 hash passwords in java. - if (!user.getPassword().equals(password)) { + if (!user.getPassword().equals(encode(password))) { s_logger.debug("Password does not match"); return false; } @@ -87,4 +67,25 @@ public class MD5UserAuthenticator extends DefaultUserAuthenticator { _userAccountDao = locator.getDao(UserAccountDao.class); return true; } + + @Override + public String encode(String password) { + MessageDigest md5 = null; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new CloudRuntimeException("Unable to hash password", e); + } + + md5.reset(); + BigInteger pwInt = new BigInteger(1, md5.digest(password.getBytes())); + String pwStr = pwInt.toString(16); + int padding = 32 - pwStr.length(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < padding; i++) { + sb.append('0'); // make sure the MD5 password is 32 digits long + } + sb.append(pwStr); + return sb.toString(); + } } diff --git a/plugins/user-authenticators/plain-text/src/com/cloud/server/auth/PlainTextUserAuthenticator.java b/plugins/user-authenticators/plain-text/src/com/cloud/server/auth/PlainTextUserAuthenticator.java index 006daf98e9b..59e12e50048 100644 --- a/plugins/user-authenticators/plain-text/src/com/cloud/server/auth/PlainTextUserAuthenticator.java +++ b/plugins/user-authenticators/plain-text/src/com/cloud/server/auth/PlainTextUserAuthenticator.java @@ -87,4 +87,10 @@ public class PlainTextUserAuthenticator extends DefaultUserAuthenticator { _userAccountDao = locator.getDao(UserAccountDao.class); return true; } + + @Override + public String encode(String password) { + // Plaintext so no encoding at all + return password; + } } diff --git a/plugins/user-authenticators/sha256salted/pom.xml b/plugins/user-authenticators/sha256salted/pom.xml new file mode 100644 index 00000000000..3f530f76e17 --- /dev/null +++ b/plugins/user-authenticators/sha256salted/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + cloud-plugin-user-authenticator-sha256salted + Apache CloudStack Plugin - User Authenticator SHA256 Salted + + org.apache.cloudstack + cloudstack-plugins + 4.1.0-SNAPSHOT + ../../pom.xml + + diff --git a/plugins/user-authenticators/sha256salted/src/com/cloud/server/auth/SHA256SaltedUserAuthenticator.java b/plugins/user-authenticators/sha256salted/src/com/cloud/server/auth/SHA256SaltedUserAuthenticator.java new file mode 100644 index 00000000000..26c33a5a9ec --- /dev/null +++ b/plugins/user-authenticators/sha256salted/src/com/cloud/server/auth/SHA256SaltedUserAuthenticator.java @@ -0,0 +1,122 @@ +// 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 com.cloud.server.auth; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; +import org.bouncycastle.util.encoders.Base64; + +import com.cloud.server.ManagementServer; +import com.cloud.servlet.CloudStartupServlet; +import com.cloud.user.UserAccount; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.component.Inject; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={UserAuthenticator.class}) +public class SHA256SaltedUserAuthenticator extends DefaultUserAuthenticator { + public static final Logger s_logger = Logger.getLogger(SHA256SaltedUserAuthenticator.class); + + @Inject + private UserAccountDao _userAccountDao; + private static int s_saltlen = 20; + + public boolean configure(String name, Map params) + throws ConfigurationException { + super.configure(name, params); + return true; + } + + /* (non-Javadoc) + * @see com.cloud.server.auth.UserAuthenticator#authenticate(java.lang.String, java.lang.String, java.lang.Long, java.util.Map) + */ + @Override + public boolean authenticate(String username, String password, + Long domainId, Map requestParameters) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Retrieving user: " + username); + } + UserAccount user = _userAccountDao.getUserAccount(username, domainId); + if (user == null) { + s_logger.debug("Unable to find user with " + username + " in domain " + domainId); + return false; + } + + try { + String storedPassword[] = user.getPassword().split(":"); + if (storedPassword.length != 2) { + s_logger.warn("The stored password for " + username + " isn't in the right format for this authenticator"); + return false; + } + byte salt[] = Base64.decode(storedPassword[0]); + String hashedPassword = encode(password, salt); + return storedPassword[1].equals(hashedPassword); + } catch (NoSuchAlgorithmException e) { + throw new CloudRuntimeException("Unable to hash password", e); + } catch (UnsupportedEncodingException e) { + throw new CloudRuntimeException("Unable to hash password", e); + } + } + + /* (non-Javadoc) + * @see com.cloud.server.auth.UserAuthenticator#encode(java.lang.String) + */ + @Override + public String encode(String password) { + // 1. Generate the salt + SecureRandom randomGen; + try { + randomGen = SecureRandom.getInstance("SHA1PRNG"); + + byte salt[] = new byte[s_saltlen]; + randomGen.nextBytes(salt); + + String saltString = new String(Base64.encode(salt)); + String hashString = encode(password, salt); + + // 3. concatenate the two and return + return saltString + ":" + hashString; + } catch (NoSuchAlgorithmException e) { + throw new CloudRuntimeException("Unable to hash password", e); + } catch (UnsupportedEncodingException e) { + throw new CloudRuntimeException("Unable to hash password", e); + } + } + + public String encode(String password, byte[] salt) throws UnsupportedEncodingException, NoSuchAlgorithmException { + byte[] passwordBytes = password.getBytes("UTF-8"); + byte[] hashSource = new byte[passwordBytes.length + s_saltlen]; + System.arraycopy(passwordBytes, 0, hashSource, 0, passwordBytes.length); + System.arraycopy(salt, 0, hashSource, passwordBytes.length, s_saltlen); + + // 2. Hash the password with the salt + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(hashSource); + byte[] digest = md.digest(); + + return new String(Base64.encode(digest)); + } +} diff --git a/plugins/user-authenticators/sha256salted/test/src/com/cloud/server/auth/test/AuthenticatorTest.java b/plugins/user-authenticators/sha256salted/test/src/com/cloud/server/auth/test/AuthenticatorTest.java new file mode 100644 index 00000000000..cf990248f39 --- /dev/null +++ b/plugins/user-authenticators/sha256salted/test/src/com/cloud/server/auth/test/AuthenticatorTest.java @@ -0,0 +1,46 @@ +package src.com.cloud.server.auth.test; + +import static org.junit.Assert.*; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import javax.naming.ConfigurationException; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.Before; +import org.junit.Test; + +import com.cloud.server.auth.SHA256SaltedUserAuthenticator; + +public class AuthenticatorTest { + + @Before + public void setUp() throws Exception { + } + + @Test + public void testEncode() throws UnsupportedEncodingException, NoSuchAlgorithmException { + SHA256SaltedUserAuthenticator authenticator = + new SHA256SaltedUserAuthenticator(); + + try { + authenticator.configure("SHA256", Collections.emptyMap()); + } catch (ConfigurationException e) { + fail(e.toString()); + } + + String encodedPassword = authenticator.encode("password"); + + String storedPassword[] = encodedPassword.split(":"); + assertEquals ("hash must consist of two components", storedPassword.length, 2); + + byte salt[] = Base64.decode(storedPassword[0]); + String hashedPassword = authenticator.encode("password", salt); + + assertEquals("compare hashes", storedPassword[1], hashedPassword); + + } + +} diff --git a/server/src/com/cloud/server/ConfigurationServerImpl.java b/server/src/com/cloud/server/ConfigurationServerImpl.java index 3368c9ba116..904e8c59f6e 100755 --- a/server/src/com/cloud/server/ConfigurationServerImpl.java +++ b/server/src/com/cloud/server/ConfigurationServerImpl.java @@ -32,6 +32,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -85,6 +86,7 @@ import com.cloud.offerings.NetworkOfferingServiceMapVO; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; +import com.cloud.server.auth.UserAuthenticator; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; @@ -96,6 +98,7 @@ import com.cloud.user.User; import com.cloud.user.dao.AccountDao; import com.cloud.utils.PasswordGenerator; import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.Adapters; import com.cloud.utils.component.ComponentLocator; import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.db.DB; @@ -342,30 +345,13 @@ public class ConfigurationServerImpl implements ConfigurationServer { } catch (SQLException ex) { } - // insert admin user + // insert admin user, but leave the account disabled until we set a + // password with the user authenticator long id = 2; String username = "admin"; String firstname = "admin"; String lastname = "cloud"; - String password = "password"; - - MessageDigest md5 = null; - try { - md5 = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - return; - } - - md5.reset(); - BigInteger pwInt = new BigInteger(1, md5.digest(password.getBytes())); - String pwStr = pwInt.toString(16); - int padding = 32 - pwStr.length(); - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < padding; i++) { - sb.append('0'); // make sure the MD5 password is 32 digits long - } - sb.append(pwStr); - + // create an account for the admin user first insertSql = "INSERT INTO `cloud`.`account` (id, account_name, type, domain_id) VALUES (" + id + ", '" + username + "', '1', '1')"; txn = Transaction.currentTxn(); @@ -376,8 +362,8 @@ public class ConfigurationServerImpl implements ConfigurationServer { } // now insert the user - insertSql = "INSERT INTO `cloud`.`user` (id, username, password, account_id, firstname, lastname, created) " + - "VALUES (" + id + ",'" + username + "','" + sb.toString() + "', 2, '" + firstname + "','" + lastname + "',now())"; + insertSql = "INSERT INTO `cloud`.`user` (id, username, account_id, firstname, lastname, created, state) " + + "VALUES (" + id + ",'" + username + "', 2, '" + firstname + "','" + lastname + "',now(), 'disabled')"; txn = Transaction.currentTxn(); try { diff --git a/server/src/com/cloud/server/ManagementServer.java b/server/src/com/cloud/server/ManagementServer.java index 473b0ee9b3a..91f82f874d5 100755 --- a/server/src/com/cloud/server/ManagementServer.java +++ b/server/src/com/cloud/server/ManagementServer.java @@ -95,4 +95,6 @@ public interface ManagementServer extends ManagementService { Pair, Integer> searchForStoragePools(Criteria c); String getHashKey(); + + public void enableAdminUser(String password); } diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index a916eb69696..117be5797b0 100755 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -177,6 +177,7 @@ import com.cloud.projects.Project.ListProjectResourcesCriteria; import com.cloud.projects.ProjectManager; import com.cloud.resource.ResourceManager; import com.cloud.server.ResourceTag.TaggedResourceType; +import com.cloud.server.auth.UserAuthenticator; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; @@ -215,7 +216,9 @@ import com.cloud.user.AccountVO; import com.cloud.user.SSHKeyPair; import com.cloud.user.SSHKeyPairVO; import com.cloud.user.User; +import com.cloud.user.UserAccount; import com.cloud.user.UserContext; +import com.cloud.user.UserVO; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.SSHKeyPairDao; import com.cloud.user.dao.UserDao; @@ -338,6 +341,8 @@ public class ManagementServerImpl implements ManagementServer { private final StatsCollector _statsCollector; private final Map _availableIdsMap; + + private Adapters _userAuthenticators; private String _hashKey = null; @@ -417,6 +422,11 @@ public class ManagementServerImpl implements ManagementServer { for (String id : availableIds) { _availableIdsMap.put(id, true); } + + _userAuthenticators = locator.getAdapters(UserAuthenticator.class); + if (_userAuthenticators == null || !_userAuthenticators.isSet()) { + s_logger.error("Unable to find an user authenticator."); + } } protected Map getConfigs() { @@ -3587,5 +3597,28 @@ public class ManagementServerImpl implements ManagementServer { } } + + public void enableAdminUser(String password) { + String encodedPassword = null; + + UserVO adminUser = _userDao.getUser(2); + if (adminUser.getState() == Account.State.disabled) { + // This means its a new account, set the password using the authenticator + + for (Enumeration en = _userAuthenticators.enumeration(); en.hasMoreElements();) { + UserAuthenticator authenticator = en.nextElement(); + encodedPassword = authenticator.encode(password); + if (encodedPassword != null) { + break; + } + } + + adminUser.setPassword(encodedPassword); + adminUser.setState(Account.State.enabled); + _userDao.persist(adminUser); + s_logger.info("Admin user enabled"); + } + + } } diff --git a/server/src/com/cloud/server/auth/UserAuthenticator.java b/server/src/com/cloud/server/auth/UserAuthenticator.java index 725516c096f..95c4f0e4707 100644 --- a/server/src/com/cloud/server/auth/UserAuthenticator.java +++ b/server/src/com/cloud/server/auth/UserAuthenticator.java @@ -34,4 +34,10 @@ public interface UserAuthenticator extends Adapter { * @return true if the user has been successfully authenticated, false otherwise */ public boolean authenticate(String username, String password, Long domainId, Map requestParameters); + + /** + * @param password + * @return the encoded password + */ + public String encode(String password); } diff --git a/server/src/com/cloud/servlet/CloudStartupServlet.java b/server/src/com/cloud/servlet/CloudStartupServlet.java index c3e5a8235ce..9efb4ea5a8c 100755 --- a/server/src/com/cloud/servlet/CloudStartupServlet.java +++ b/server/src/com/cloud/servlet/CloudStartupServlet.java @@ -47,6 +47,7 @@ public class CloudStartupServlet extends HttpServlet implements ServletContextLi c.persistDefaultValues(); s_locator = ComponentLocator.getLocator(ManagementServer.Name); ManagementServer ms = (ManagementServer)ComponentLocator.getComponent(ManagementServer.Name); + ms.enableAdminUser("password"); ApiServer.initApiServer(ms.getApiConfig()); } catch (InvalidParameterValueException ipve) { s_logger.error("Exception starting management server ", ipve); diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index f1e606e76a1..0def0083f66 100755 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -921,7 +921,18 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag } if (password != null) { - user.setPassword(password); + String encodedPassword = null; + for (Enumeration en = _userAuthenticators.enumeration(); en.hasMoreElements();) { + UserAuthenticator authenticator = en.nextElement(); + encodedPassword = authenticator.encode(password); + if (encodedPassword != null) { + break; + } + } + if (encodedPassword == null) { + throw new CloudRuntimeException("Failed to encode password"); + } + user.setPassword(encodedPassword); } if (email != null) { user.setEmail(email); @@ -1670,7 +1681,20 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag if (s_logger.isDebugEnabled()) { s_logger.debug("Creating user: " + userName + ", accountId: " + accountId + " timezone:" + timezone); } - UserVO user = _userDao.persist(new UserVO(accountId, userName, password, firstName, lastName, email, timezone)); + + String encodedPassword = null; + for (Enumeration en = _userAuthenticators.enumeration(); en.hasMoreElements();) { + UserAuthenticator authenticator = en.nextElement(); + encodedPassword = authenticator.encode(password); + if (encodedPassword != null) { + break; + } + } + if (encodedPassword == null) { + throw new CloudRuntimeException("Failed to encode password"); + } + + UserVO user = _userDao.persist(new UserVO(accountId, userName, encodedPassword, firstName, lastName, email, timezone)); return user; } diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index 5e187ed1b82..28b3bb9550f 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -37,8 +37,8 @@ var ERROR_INTERNET_CANNOT_CONNECT = 12029; var ERROR_VMOPS_ACCOUNT_ERROR = 531; // Default password is MD5 hashed. Set the following variable to false to disable this. -var md5Hashed = true; -var md5HashedLogin = true; +var md5Hashed = false; +var md5HashedLogin = false; //page size for API call (e.g."listXXXXXXX&pagesize=N" ) var pageSize = 20;