// 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 static java.lang.String.format; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Map; import javax.inject.Inject; 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 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.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; public class PBKDF2UserAuthenticator extends AdapterBase implements UserAuthenticator { 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; @Override public Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, Map<String, Object[]> requestParameters) { if (s_logger.isDebugEnabled()) { s_logger.debug("Retrieving user: " + username); } if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { s_logger.debug("Username or Password cannot be empty"); return new Pair<Boolean, ActionOnFailedAuthentication>(false, null); } 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>:<password hash>:<rounds> 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); } } @Override 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) throws UnsupportedEncodingException { return new String(Base64.encode(input), "UTF-8"); } private static byte[] decode(String input) throws UnsupportedEncodingException { return Base64.decode(input.getBytes("UTF-8")); } }