001/*
002 * (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.directory;
018
019import java.io.UnsupportedEncodingException;
020import java.security.MessageDigest;
021import java.security.NoSuchAlgorithmException;
022import java.security.SecureRandom;
023import java.util.Random;
024
025import org.nuxeo.common.utils.Base64;
026
027/**
028 * Helper to check passwords and generated hashed salted ones.
029 */
030public class PasswordHelper {
031
032    public static final String SSHA = "SSHA";
033
034    public static final String SMD5 = "SMD5";
035
036    private static final String HSSHA = "{SSHA}";
037
038    private static final String HSMD5 = "{SMD5}";
039
040    private static final String SHA1 = "SHA-1";
041
042    private static final String MD5 = "MD5";
043
044    private static final int SALT_LEN = 8;
045
046    private static final Random random = new SecureRandom();
047
048    // utility class
049    private PasswordHelper() {
050    }
051
052    /**
053     * Checks if a password is already hashed.
054     *
055     * @param password
056     * @return {@code true} if the password is hashed
057     */
058    public static boolean isHashed(String password) {
059        return password.startsWith(HSSHA) || password.startsWith(HSMD5);
060    }
061
062    /**
063     * Returns the hashed string for a password according to a given hashing algorithm.
064     *
065     * @param algorithm the algorithm, {@link #SSHA} or {@link #SMD5}, or {@code null} to not hash
066     * @param password the password
067     * @return the hashed password
068     */
069    public static String hashPassword(String password, String algorithm) {
070        if (algorithm == null || "".equals(algorithm)) {
071            return password;
072        }
073        String digestalg;
074        String prefix;
075        if (SSHA.equals(algorithm)) {
076            digestalg = SHA1;
077            prefix = HSSHA;
078        } else if (SMD5.equals(algorithm)) {
079            digestalg = MD5;
080            prefix = HSMD5;
081        } else {
082            throw new RuntimeException("Unknown algorithm: " + algorithm);
083        }
084
085        byte[] salt = new byte[SALT_LEN];
086        synchronized (random) {
087            random.nextBytes(salt);
088        }
089        byte[] hash = digestWithSalt(password, salt, digestalg);
090        byte[] bytes = new byte[hash.length + salt.length];
091        System.arraycopy(hash, 0, bytes, 0, hash.length);
092        System.arraycopy(salt, 0, bytes, hash.length, salt.length);
093        return prefix + Base64.encodeBytes(bytes);
094    }
095
096    /**
097     * Verify a password against a hashed password.
098     *
099     * @param password the password to verify
100     * @param hashedPassword the hashed password
101     * @return {@code true} if the password matches
102     */
103    public static boolean verifyPassword(String password, String hashedPassword) {
104        String digestalg;
105        int len;
106        if (hashedPassword.startsWith(HSSHA)) {
107            digestalg = SHA1;
108            len = 20;
109        } else if (hashedPassword.startsWith(HSMD5)) {
110            digestalg = MD5;
111            len = 16;
112        } else {
113            return hashedPassword.equals(password);
114        }
115        String digest = hashedPassword.substring(6);
116
117        byte[] bytes = Base64.decode(digest);
118        if (bytes == null) {
119            // invalid base64
120            return false;
121        }
122        if (bytes.length < len + 2) {
123            // needs hash + at least two bytes of salt
124            return false;
125        }
126        byte[] hash = new byte[len];
127        byte[] salt = new byte[bytes.length - len];
128        System.arraycopy(bytes, 0, hash, 0, hash.length);
129        System.arraycopy(bytes, hash.length, salt, 0, salt.length);
130        return MessageDigest.isEqual(hash, digestWithSalt(password, salt, digestalg));
131    }
132
133    public static byte[] digestWithSalt(String password, byte[] salt, String algorithm) {
134        try {
135            MessageDigest md = MessageDigest.getInstance(algorithm);
136            md.update(password.getBytes("UTF-8"));
137            md.update(salt);
138            return md.digest();
139        } catch (NoSuchAlgorithmException e) {
140            throw new RuntimeException(algorithm, e);
141        } catch (UnsupportedEncodingException e) {
142            throw new RuntimeException(e);
143        }
144    }
145
146}