001/*
002 * (C) Copyright 2010 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.nuxeo.common.utils.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     * @param password
058     * @return {@code true} if the password is hashed
059     */
060    public static boolean isHashed(String password) {
061        return password.startsWith(HSSHA) || password.startsWith(HSMD5);
062    }
063
064    /**
065     * Returns the hashed string for a password according to a given hashing algorithm.
066     *
067     * @param algorithm the algorithm, {@link #SSHA} or {@link #SMD5}, or {@code null} to not hash
068     * @param password the password
069     * @return the hashed password
070     */
071    public static String hashPassword(String password, String algorithm) {
072        if (algorithm == null || "".equals(algorithm)) {
073            return password;
074        }
075        String digestalg;
076        String prefix;
077        if (SSHA.equals(algorithm)) {
078            digestalg = SHA1;
079            prefix = HSSHA;
080        } else if (SMD5.equals(algorithm)) {
081            digestalg = MD5;
082            prefix = HSMD5;
083        } else {
084            throw new RuntimeException("Unknown algorithm: " + algorithm);
085        }
086
087        byte[] salt = new byte[SALT_LEN];
088        synchronized (random) {
089            random.nextBytes(salt);
090        }
091        byte[] hash = digestWithSalt(password, salt, digestalg);
092        byte[] bytes = new byte[hash.length + salt.length];
093        System.arraycopy(hash, 0, bytes, 0, hash.length);
094        System.arraycopy(salt, 0, bytes, hash.length, salt.length);
095        return prefix + Base64.encodeBytes(bytes);
096    }
097
098    /**
099     * Verify a password against a hashed password.
100     *
101     * @param password the password to verify
102     * @param hashedPassword the hashed password
103     * @return {@code true} if the password matches
104     */
105    public static boolean verifyPassword(String password, String hashedPassword) {
106        String digestalg;
107        int len;
108        if (hashedPassword.startsWith(HSSHA)) {
109            digestalg = SHA1;
110            len = 20;
111        } else if (hashedPassword.startsWith(HSMD5)) {
112            digestalg = MD5;
113            len = 16;
114        } else {
115            return hashedPassword.equals(password);
116        }
117        String digest = hashedPassword.substring(6);
118
119        byte[] bytes = Base64.decode(digest);
120        if (bytes == null) {
121            // invalid base64
122            return false;
123        }
124        if (bytes.length < len + 2) {
125            // needs hash + at least two bytes of salt
126            return false;
127        }
128        byte[] hash = new byte[len];
129        byte[] salt = new byte[bytes.length - len];
130        System.arraycopy(bytes, 0, hash, 0, hash.length);
131        System.arraycopy(bytes, hash.length, salt, 0, salt.length);
132        return MessageDigest.isEqual(hash, digestWithSalt(password, salt, digestalg));
133    }
134
135    public static byte[] digestWithSalt(String password, byte[] salt, String algorithm) {
136        try {
137            MessageDigest md = MessageDigest.getInstance(algorithm);
138            md.update(password.getBytes("UTF-8"));
139            md.update(salt);
140            return md.digest();
141        } catch (NoSuchAlgorithmException e) {
142            throw new RuntimeException(algorithm, e);
143        } catch (UnsupportedEncodingException e) {
144            throw new RuntimeException(e);
145        }
146    }
147
148}