/* * Copyright (C) 2006-2014 Glencoe Software, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package ome.security.auth; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.List; import java.util.Random; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.security.SecuritySystem; import ome.system.Roles; import ome.util.SqlAction; import ome.util.checksum.ChecksumProviderFactory; import ome.util.checksum.ChecksumProviderFactoryImpl; import ome.util.checksum.ChecksumType; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Static methods for dealing with password hashes and the "password" table. * Used primarily by {@link ome.logic.AdminImpl} * * @author Josh Moore, josh.moore at gmx.de * @see SecuritySystem * @see ome.logic.AdminImpl * @since 3.0-Beta1 */ public class PasswordUtil { public static enum METHOD { CLEAR(false, false), LEGACY(true, false), ALL(true, true); private final boolean hash; private final boolean salt; METHOD(boolean hash, boolean salt) { this.hash = hash; this.salt = salt; } } /** * The default encoding for converting plain text passwords to byte arrays * (UTF-8) */ public final static String DEFAULT_ENCODING = "UTF-8"; private final static Logger log = LoggerFactory.getLogger(PasswordUtil.class); private final SqlAction sql; private final Roles roles; private final boolean passwordRequired; private final Charset encoding; public PasswordUtil(SqlAction sql) { this(sql, new Roles(), true); } public PasswordUtil(SqlAction sql, boolean passwordRequired) { this(sql, new Roles(), passwordRequired); } public PasswordUtil(SqlAction sql, Charset encoding) { this(sql, true, encoding); } public PasswordUtil(SqlAction sql, boolean passwordRequired, Charset encoding) { this(sql, new Roles(), passwordRequired, encoding); } public PasswordUtil(SqlAction sql, Roles roles, boolean passwordRequired) { this(sql, roles, passwordRequired, Charset.forName(DEFAULT_ENCODING)); } public PasswordUtil(SqlAction sql, Roles roles, boolean passwordRequired, Charset encoding) { this.sql = sql; this.roles = roles; this.passwordRequired = passwordRequired; this.encoding = encoding; } /** * Main method which takes exactly one argument, passes it to * {@link #preparePassword(String)} and prints the results on * {@link System#out}. This is used by the build system to define the * "@ROOTPASS@" placeholder in data.sql. * @param args the command-line arguments */ public static void main(String args[]) { if (args == null || args.length < 1 || args.length > 2) { throw new IllegalArgumentException("PasswordUtil password [user-id]"); } PasswordUtil util = new PasswordUtil(null); String pw = args[0]; if (args.length == 1) { System.out.println(util.preparePassword(pw)); } else { Long userId = Long.valueOf(args[1]); System.out.println(util.prepareSaltedPassword(userId, pw)); } } public String generateRandomPasswd() { StringBuffer buffer = new StringBuffer(); Random random = new Random(); char[] chars = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; for (int i = 0; i < 10; i++) { buffer.append(chars[random.nextInt(chars.length)]); } return buffer.toString(); } public boolean getDnById(Long id) { return sql.isLdapExperimenter(id); } /** * Calls {@link #changeUserPasswordById(Long, String, METHOD)} with * "false" as the value of the salt argument in order to provide backwards * compatibility. * @param id the user ID * @param password the password */ public void changeUserPasswordById(Long id, String password) { changeUserPasswordById(id, password, METHOD.LEGACY); } /** * Calls either {@link #preparePassword(String)} or * {@link #prepareSaltedPassword(Long, String)} and passes the resulting * value to {@link SqlAction#setUserPassword(Long, String)}. * An {@link InternalException} is thrown if the modification is not * successful, which should only occur if the user has been deleted. * @param id the user ID * @param password the password * @param meth how to encode the password */ public void changeUserPasswordById(Long id, String password, METHOD meth) { String prepared = password; if (meth.hash){ prepared = preparePassword(id, password, meth.salt); } if (! sql.setUserPassword(id, prepared)) { throw new InternalException("0 results for password insert."); } sql.clearPermissionsBit("experimenter", id, 16); } public String getUserPasswordHash(Long id) { return sql.getPasswordHash(id); } /** * Get the user's ID * @param name the user's name * @return their ID, or {@code null} if they cannot be found */ public Long userId(String name) { return sql.getUserId(name); } /** * Get the user's name * @param id the user's ID * @return their name, or {@code null} if they cannot be found */ public String userName(long id) { return sql.getUsername(id); } public List<String> userGroups(String name) { return sql.getUserGroups(name); } public String preparePassword(String newPassword) { return preparePassword(null, newPassword, false); } public String prepareSaltedPassword(Long userId, String newPassword) { return preparePassword(userId, newPassword, true); } protected String preparePassword(Long userId, String newPassword, boolean salt) { // This allows setting passwords to "null" - locked account. // Also checks if empty passwords are to be considered "open-access" return newPassword == null || (newPassword.trim().isEmpty() && isPasswordRequired(userId)) ? null : newPassword.trim().isEmpty() ? newPassword // Regular MD5 digest. : passwordDigest(userId, newPassword, salt); } /** * Creates an MD5 hash of the given clear text and base64 encodes it. * @param clearText the cleartext of the password * @return the password hash */ // TODO This should almost certainly be configurable as to encoding, // algorithm, and possibly even the implementation in general. public String passwordDigest(String clearText) { return passwordDigest(null, clearText, false); } /** * Creates an MD5 hash of the given clear text and base64 encodes it. * If the provided userId argument is not null, then it will be used * as a salt value for the password. * @param userId the user's ID, may be {@code null} * @param clearText the cleartext of the password * @return the password hash */ public String saltedPasswordDigest(Long userId, String clearText) { return passwordDigest(userId, clearText, true); } protected String passwordDigest(Long userId, String clearText, boolean salt) { if (clearText == null) { throw new ApiUsageException("Value for digesting may not be null"); } byte[] bytes = clearText.getBytes(encoding); // If salting is activated, prepend the salt. if (userId != null && salt) { byte[] saltedBytes = ByteBuffer.allocate(8).putLong(userId).array(); byte[] newValue = new byte[saltedBytes.length+bytes.length]; System.arraycopy(saltedBytes, 0, newValue, 0, saltedBytes.length); System.arraycopy(bytes, 0, newValue, saltedBytes.length, bytes.length); bytes = newValue; } String hashedText = null; ChecksumProviderFactory cpf = new ChecksumProviderFactoryImpl(); try { bytes = cpf.getProvider(ChecksumType.MD5).putBytes(bytes) .checksumAsBytes(); bytes = Base64.encodeBase64(bytes); hashedText = new String(bytes); } catch (Exception e) { log.error("Could not hash password", e); } if (hashedText == null) { throw new InternalException("Failed to obtain digest."); } return hashedText; } /** * Returns a boolean based on the supplied user ID and system property * setting. If password requirement is switched off or the user is * a guest user, then this returns <code>false</code>. In all other cases * this returns <code>true</code>. * * @param id The user ID. * @return boolean <code>true</code> or <code>false</code> */ public boolean isPasswordRequired(Long id) { if (id == null) { return passwordRequired; } else { return !id.equals(roles.getGuestId()) && passwordRequired; } } }