/*
* Copyright 2004 - 2009 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id: Constants.java 11478 2010-02-01 15:25:42Z tot $
*/
package de.dal33t.powerfolder.util;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
/**
* Utility class for login helpers
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.29 $
*/
public class LoginUtil {
private LoginUtil() {
}
private static final int OBF_BYTE = 0xAA;
public static final String MD5_HASH_DIGEST = "MD5";
public static final String SHA256_HASH_DIGEST = "SHA-256";
/**
* Obfuscates a password into String. This does NOT mean the password is
* Encrypted or secure, but it prevents accidentally reveal.
* <p>
* Passwords can easily deobfuscated by calling {@link #deobfuscate(String)}
*
* @param password
* the password to obfuscate
* @return the obfuscated password
*/
public static String obfuscate(char[] password) {
if (password == null) {
return null;
}
CharBuffer cBuf = CharBuffer.wrap(password);
byte[] buf = new byte[password.length * 3];
ByteBuffer bBuf = ByteBuffer.wrap(buf);
CharsetEncoder enc = Convert.UTF8.newEncoder();
enc.encode(cBuf, bBuf, true);
int len = bBuf.position();
if (len != buf.length) {
buf = Arrays.copyOf(buf, len);
}
for (int i = 0; i < buf.length; i++) {
buf[i] = (byte) (buf[i] ^ OBF_BYTE);
buf[i] = (byte) (buf[i] + 127);
}
return Base64.encodeBytes(buf, Base64.DONT_BREAK_LINES);
}
/**
* Deobfuscates a obfuscated password by {@link #obfuscate(char[])}
*
* @param passwordOBF
* the obfuscated password
* @return the original password
*/
public static char[] deobfuscate(String passwordOBF) {
if (passwordOBF == null) {
return null;
}
try {
byte[] buf = Base64.decode(passwordOBF, Base64.DONT_BREAK_LINES);
for (int i = 0; i < buf.length; i++) {
buf[i] = (byte) (buf[i] - 127);
buf[i] = (byte) (buf[i] ^ OBF_BYTE);
}
ByteBuffer bBuf = ByteBuffer.wrap(buf);
char[] ca = new char[buf.length];
CharBuffer cBuf = CharBuffer.wrap(ca);
CharsetDecoder dec = Convert.UTF8.newDecoder();
dec.decode(bBuf, cBuf, true);
int len = cBuf.position();
if (len != ca.length) {
ca = Arrays.copyOf(ca, len);
}
return ca;
} catch (Exception e) {
Logger.getLogger(LoginUtil.class.getName()).log(
Level.SEVERE,
"Unable to decode obfuscated password: " + passwordOBF + ". "
+ e, e);
return null;
}
}
/**
* Decorates the login URL with credentials if given.
*
* @param loginURL
* the login URL, e.g. http://localhost/login
* @param username
* @param password
* @return the login URL with encoded credentials as parameters.
*/
public static String decorateURL(String loginURL, String username,
char[] password)
{
String url = loginURL;
if (StringUtils.isNotBlank(username)) {
url += "?";
url += Constants.LOGIN_PARAM_USERNAME;
url += "=";
url += Util.endcodeForURL(username);
if (password != null && password.length > 0) {
url += "&";
url += Constants.LOGIN_PARAM_PASSWORD_OBF;
url += "=";
url += Util.endcodeForURL(obfuscate(password));
}
}
return url;
}
/**
* Decorates the login URL with credentials if given.
*
* @param loginURL
* the login URL, e.g. http://localhost/login
* @param username
* @param passwordObf
* the obfuscated password
* @return the login URL with encoded credentials as parameters.
*/
public static String decorateURL(String loginURL, String username,
String passwordObf)
{
String url = loginURL;
if (StringUtils.isNotBlank(username)) {
url += "?";
url += Constants.LOGIN_PARAM_USERNAME;
url += "=";
url += Util.endcodeForURL(username);
if (StringUtils.isNotBlank(passwordObf)) {
url += "&";
url += Constants.LOGIN_PARAM_PASSWORD_OBF;
url += "=";
url += Util.endcodeForURL(passwordObf);
}
}
return url;
}
public static boolean matches(char[] pwCandidate, String hashedPW) {
String[] parts = hashedPW.split(":");
if (parts.length != 3) {
// Legacy for clear text passwords
return hashedPW != null && !hashedPW.startsWith(MD5_HASH_DIGEST)
&& !hashedPW.startsWith(SHA256_HASH_DIGEST)
&& Arrays.equals(pwCandidate, Util.toCharArray(hashedPW));
}
String digest = parts[0];
if (digest.equalsIgnoreCase(MD5_HASH_DIGEST)
|| digest.equalsIgnoreCase(SHA256_HASH_DIGEST))
{
String salt = parts[1];
String expectedHash = parts[2];
String actualHash = hash(digest, Util.toString(pwCandidate), salt);
return expectedHash.equals(actualHash);
}
return false;
}
/**
* @param password
* the password to process
* @return the hashed password and salt.
*/
public static String hashAndSalt(String password) {
String salt = IdGenerator.makeId();
String digest = getPreferredDigest().getAlgorithm();
return digest + ':' + salt + ':' + hash(digest, password, salt);
}
/**
* @param password
* the password to process
* @return the hashed password and salt.
*/
public static String hash(String digest, String password, String salt) {
String input = password + salt;
byte[] in = input.getBytes(Convert.UTF8);
for (int i = 0; i < 1597; i++) {
in = digest(digest, in);
}
return Base64.encodeBytes(in);
}
/**
* Clears a password array to avoid keeping the password in plain text in
* memory.
*
* @param password
* the password array to clear. Array is destroyed and unusable
* after.
*/
public static void clear(char[] password) {
if (password == null || password.length == 0) {
return;
}
for (int i = 0; i < password.length; i++) {
password[i] = (char) (Math.random() * 256);
}
}
/**
* PFS-569: Hack alert!
*
* @param controller
* @return
*/
public static String getInviteUsernameLabel(Controller controller) {
if (isUsernameShibboleth(controller)) {
return Translation.getTranslation("general.email") + ':';
}
return getUsernameLabel(controller);
}
/**
* #2401: Texts: "Email" should not be shown if using AD username, e.g. on
* login
*
* @param controller
* @return
*/
public static String getUsernameLabel(Controller controller) {
return getUsernameText(controller) + ':';
}
/**
* #2401: Texts: "Email" should not be shown if using AD username, e.g. on
* login
*
* @param controller
* @return
*/
public static String getUsernameText(Controller controller) {
Reject.ifNull(controller, "Controller");
if (isUsernameEmailOnly(controller)) {
return Translation.getTranslation("general.email");
} else if (isUsernameAny(controller)) {
return Translation.getTranslation("general.username") + '/'
+ Translation.getTranslation("general.email");
} else if (isUsernameShibboleth(controller)) {
// FIXME: Make nicer
return "Login-ID";
} else {
if (isBoolConfValue(controller)) {
return Translation.getTranslation("general.username");
}
// Otherwise return text of config entry.
return ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValue(controller);
}
}
public static boolean isValidUsername(Controller controller, String username)
{
if (StringUtils.isBlank(username)) {
return false;
}
if (isUsernameAny(controller)) {
return true;
}
// PFS-569
if (isUsernameShibboleth(controller)) {
return username.contains(Constants.SHIBBOLETH_USERNAME_SEPARATOR)
|| username.contains("@");
}
if (ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValueBoolean(controller))
{
return Util.isValidEmail(username);
}
return true;
}
private static boolean isUsernameShibboleth(Controller controller) {
String v = ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValue(controller);
if (v == null) {
return false;
}
return v.toLowerCase().contains("shibboleth")
|| v.toLowerCase().contains("bwidm");
}
private static boolean isUsernameEmailOnly(Controller controller) {
if (isUsernameAny(controller)) {
return false;
}
if (!isBoolConfValue(controller)) {
return false;
}
return ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValueBoolean(controller);
}
private static boolean isUsernameAny(Controller controller) {
String v = ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValue(controller);
return "both".equalsIgnoreCase(v);
}
private static boolean isBoolConfValue(Controller controller) {
String value = ConfigurationEntry.SERVER_USERNAME_IS_EMAIL
.getValue(controller);
if (value == null) {
return false;
}
return value.equalsIgnoreCase("true")
|| value.equalsIgnoreCase("false");
}
/**
* Calculates the SHA digest and returns the value as a 16 element
* {@code byte[]}.
*
* @param data
* Data to digest
* @return digest
*/
private static byte[] digest(String digest, byte[] data) {
return getDigest(digest).digest(data);
}
/**
* Returns a MessageDigest for the given {@code algorithm}.
*
* @param algorithm
* The MessageDigest algorithm name.
* @return An MD5 digest instance.
* @throws RuntimeException
* when a {@link NoSuchAlgorithmException} is
* caught,
*/
private static MessageDigest getDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* Returns an MD5 MessageDigest.
*
* @return An MD5 digest instance.
* @throws RuntimeException
* when a {@link NoSuchAlgorithmException} is
* caught,
*/
private static MessageDigest getPreferredDigest() {
return getDigest(SHA256_HASH_DIGEST);
}
}