// 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.inject.Inject; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.bouncycastle.util.encoders.Base64; import com.cloud.user.UserAccount; import com.cloud.user.dao.UserAccountDao; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAuthenticator { public static final Logger s_logger = Logger.getLogger(SHA256SaltedUserAuthenticator.class); private static final String s_defaultPassword = "000000000000000000000000000="; private static final String s_defaultSalt = "0000000000000000000000000000000="; @Inject private UserAccountDao _userAccountDao; private static final int s_saltlen = 32; /* (non-Javadoc) * @see com.cloud.server.auth.UserAuthenticator#authenticate(java.lang.String, java.lang.String, java.lang.Long, java.util.Map) */ @Override public Pair<Boolean, 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 realUser = true; UserAccount user = _userAccountDao.getUserAccount(username, domainId); if (user == null) { s_logger.debug("Unable to find user with " + username + " in domain " + domainId); realUser = false; } /* Fake Data */ String realPassword = new String(s_defaultPassword); byte[] salt = new String(s_defaultSalt).getBytes(); if (realUser) { 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"); realUser = false; } else { realPassword = storedPassword[1]; salt = Base64.decode(storedPassword[0]); } } try { String hashedPassword = encode(password, salt); /* constantTimeEquals comes first in boolean since we need to thwart timing attacks */ boolean result = constantTimeEquals(realPassword, hashedPassword) && realUser; ActionOnFailedAuthentication action = null; if (!result && realUser) { action = ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT; } return new Pair<Boolean, ActionOnFailedAuthentication>(result, action); } 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 + salt.length]; System.arraycopy(passwordBytes, 0, hashSource, 0, passwordBytes.length); System.arraycopy(salt, 0, hashSource, passwordBytes.length, salt.length); // 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)); } private static boolean constantTimeEquals(String a, String b) { byte[] aBytes = a.getBytes(); byte[] bBytes = b.getBytes(); int result = aBytes.length ^ bBytes.length; for (int i = 0; i < aBytes.length && i < bBytes.length; i++) { result |= aBytes[i] ^ bBytes[i]; } return result == 0; } }