/*
* (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.directory;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import org.apache.commons.codec.binary.Base64;
/**
* Helper to check passwords and generated hashed salted ones.
*/
public class PasswordHelper {
public static final String SSHA = "SSHA";
public static final String SMD5 = "SMD5";
private static final String HSSHA = "{SSHA}";
private static final String HSMD5 = "{SMD5}";
private static final String SHA1 = "SHA-1";
private static final String MD5 = "MD5";
private static final int SALT_LEN = 8;
private static final Random random = new SecureRandom();
// utility class
private PasswordHelper() {
}
/**
* Checks if a password is already hashed.
*
* @return {@code true} if the password is hashed
*/
public static boolean isHashed(String password) {
return password.startsWith(HSSHA) || password.startsWith(HSMD5);
}
/**
* Returns the hashed string for a password according to a given hashing algorithm.
*
* @param algorithm the algorithm, {@link #SSHA} or {@link #SMD5}, or {@code null} to not hash
* @param password the password
* @return the hashed password
*/
public static String hashPassword(String password, String algorithm) {
if (algorithm == null || "".equals(algorithm)) {
return password;
}
String digestalg;
String prefix;
if (SSHA.equals(algorithm)) {
digestalg = SHA1;
prefix = HSSHA;
} else if (SMD5.equals(algorithm)) {
digestalg = MD5;
prefix = HSMD5;
} else {
throw new RuntimeException("Unknown algorithm: " + algorithm);
}
byte[] salt = new byte[SALT_LEN];
synchronized (random) {
random.nextBytes(salt);
}
byte[] hash = digestWithSalt(password, salt, digestalg);
byte[] bytes = new byte[hash.length + salt.length];
System.arraycopy(hash, 0, bytes, 0, hash.length);
System.arraycopy(salt, 0, bytes, hash.length, salt.length);
return prefix + Base64.encodeBase64String(bytes);
}
/**
* Verify a password against a hashed password.
* <p>
* If the hashed password is {@code null} then the verification always fails.
*
* @param password the password to verify
* @param hashedPassword the hashed password
* @return {@code true} if the password matches
*/
public static boolean verifyPassword(String password, String hashedPassword) {
if (hashedPassword == null) {
return false;
}
String digestalg;
int len;
if (hashedPassword.startsWith(HSSHA)) {
digestalg = SHA1;
len = 20;
} else if (hashedPassword.startsWith(HSMD5)) {
digestalg = MD5;
len = 16;
} else {
return hashedPassword.equals(password);
}
String digest = hashedPassword.substring(6);
byte[] bytes = Base64.decodeBase64(digest);
if (bytes == null) {
// invalid base64
return false;
}
if (bytes.length < len + 2) {
// needs hash + at least two bytes of salt
return false;
}
byte[] hash = new byte[len];
byte[] salt = new byte[bytes.length - len];
System.arraycopy(bytes, 0, hash, 0, hash.length);
System.arraycopy(bytes, hash.length, salt, 0, salt.length);
return MessageDigest.isEqual(hash, digestWithSalt(password, salt, digestalg));
}
public static byte[] digestWithSalt(String password, byte[] salt, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
md.update(password.getBytes("UTF-8"));
md.update(salt);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(algorithm, e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}