001/*
002 * (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.directory;
020
021import java.io.UnsupportedEncodingException;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.security.SecureRandom;
025import java.util.Random;
026
027import org.apache.commons.codec.binary.Base64;
028
029/**
030 * Helper to check passwords and generated hashed salted ones.
031 */
032public class PasswordHelper {
033
034    public static final String SSHA = "SSHA";
035
036    public static final String SMD5 = "SMD5";
037
038    private static final String HSSHA = "{SSHA}";
039
040    private static final String HSMD5 = "{SMD5}";
041
042    private static final String SHA1 = "SHA-1";
043
044    private static final String MD5 = "MD5";
045
046    private static final int SALT_LEN = 8;
047
048    private static final Random random = new SecureRandom();
049
050    // utility class
051    private PasswordHelper() {
052    }
053
054    /**
055     * Checks if a password is already hashed.
056     *
057     * @return {@code true} if the password is hashed
058     */
059    public static boolean isHashed(String password) {
060        return password.startsWith(HSSHA) || password.startsWith(HSMD5);
061    }
062
063    /**
064     * Returns the hashed string for a password according to a given hashing algorithm.
065     *
066     * @param algorithm the algorithm, {@link #SSHA} or {@link #SMD5}, or {@code null} to not hash
067     * @param password the password
068     * @return the hashed password
069     */
070    public static String hashPassword(String password, String algorithm) {
071        if (algorithm == null || "".equals(algorithm)) {
072            return password;
073        }
074        String digestalg;
075        String prefix;
076        if (SSHA.equals(algorithm)) {
077            digestalg = SHA1;
078            prefix = HSSHA;
079        } else if (SMD5.equals(algorithm)) {
080            digestalg = MD5;
081            prefix = HSMD5;
082        } else {
083            throw new RuntimeException("Unknown algorithm: " + algorithm);
084        }
085
086        byte[] salt = new byte[SALT_LEN];
087        synchronized (random) {
088            random.nextBytes(salt);
089        }
090        byte[] hash = digestWithSalt(password, salt, digestalg);
091        byte[] bytes = new byte[hash.length + salt.length];
092        System.arraycopy(hash, 0, bytes, 0, hash.length);
093        System.arraycopy(salt, 0, bytes, hash.length, salt.length);
094        return prefix + Base64.encodeBase64String(bytes);
095    }
096
097    /**
098     * Verify a password against a hashed password.
099     * <p>
100     * If the hashed password is {@code null} then the verification always fails.
101     *
102     * @param password the password to verify
103     * @param hashedPassword the hashed password
104     * @return {@code true} if the password matches
105     */
106    public static boolean verifyPassword(String password, String hashedPassword) {
107        if (hashedPassword == null) {
108            return false;
109        }
110        String digestalg;
111        int len;
112        if (hashedPassword.startsWith(HSSHA)) {
113            digestalg = SHA1;
114            len = 20;
115        } else if (hashedPassword.startsWith(HSMD5)) {
116            digestalg = MD5;
117            len = 16;
118        } else {
119            return hashedPassword.equals(password);
120        }
121        String digest = hashedPassword.substring(6);
122
123        byte[] bytes = Base64.decodeBase64(digest);
124        if (bytes == null) {
125            // invalid base64
126            return false;
127        }
128        if (bytes.length < len + 2) {
129            // needs hash + at least two bytes of salt
130            return false;
131        }
132        byte[] hash = new byte[len];
133        byte[] salt = new byte[bytes.length - len];
134        System.arraycopy(bytes, 0, hash, 0, hash.length);
135        System.arraycopy(bytes, hash.length, salt, 0, salt.length);
136        return MessageDigest.isEqual(hash, digestWithSalt(password, salt, digestalg));
137    }
138
139    public static byte[] digestWithSalt(String password, byte[] salt, String algorithm) {
140        try {
141            MessageDigest md = MessageDigest.getInstance(algorithm);
142            if (password == null) {
143                password = "";
144            }
145            md.update(password.getBytes("UTF-8"));
146            md.update(salt);
147            return md.digest();
148        } catch (NoSuchAlgorithmException e) {
149            throw new RuntimeException(algorithm, e);
150        } catch (UnsupportedEncodingException e) {
151            throw new RuntimeException(e);
152        }
153    }
154
155}