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}