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}