View Javadoc

1   /**********************************************************************************
2    * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/user/impl/OpenAuthnComponent.java $
3    * $Id: OpenAuthnComponent.java 51317 2008-08-24 04:38:02Z csev@umich.edu $
4    ***********************************************************************************
5    *
6    * Copyright (c) 2005, 2006, 2008, 2009, 2010 Sakai Foundation
7    *
8    * Licensed under the Educational Community License, Version 2.0 (the "License");
9    * you may not use this file except in compliance with the License.
10   * You may obtain a copy of the License at
11   *
12   *       http://www.osedu.org/licenses/ECL-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   *
20   **********************************************************************************/
21  
22  package org.sakaiproject.user.impl;
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.OutputStream;
26  import java.security.MessageDigest;
27  import java.util.Random;
28  
29  import javax.mail.internet.MimeUtility;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  
35  /**
36   * Service to check that a password matches and to generate encrypted passwords from plaintext.
37   * By default it's salted and SHA-256 digested.
38   * @author buckett
39   *
40   */
41  public class PasswordService {
42  
43  	private final static Log log = LogFactory.getLog(PasswordService.class);
44  
45  	private Random saltSource = new Random();
46  
47  	/**
48  	 * Prefix for password that was converted from an MD5 one.
49  	 */
50  	public static final String MD5_SALT_SHA256 = "MD5-SALT-SHA256:";
51  
52  	/**
53  	 * Prefix for password that was converted from an MD5 one with the SAK-5922 bug.
54  	 */
55  	public static final String MD5TRUNC_SALT_SHA256 = "MD5TRUNC-SALT-SHA256:";
56  
57  	/** 
58  	 * Length of salt in bytes.
59  	 */
60  	private int saltLength = 4;
61  
62  	/**
63  	 * The character that separates the salt from the hash.
64  	 * Changing this will break all existing passwords (except MD5).
65  	 */
66  	private String saltDelim = ":";
67  
68  	/**
69  	 * The default algorithm to use then setting a password and when checking a password.
70  	 * Changing this will break all existing password (except MD5).
71  	 */
72  	private String defaultAlgorithm = "SHA-256";
73  
74  	/**
75  	 * Check to see if the password matches the encrypted version.
76  	 * @param password
77  	 * @param encrypted
78  	 * @return
79  	 */
80  	public boolean check(String password, String encrypted) {
81  		// Make sure we have good data.
82  		if (password == null || encrypted == null)
83  			return false;
84  		
85  		int length = encrypted.length();
86  		// This is the old way of encrypting passwords.
87  		if ( length == 20 || length == 24 ) {
88  			String passwordEnc = hash(password, "MD5");
89  			if (passwordEnc != null) {
90  				// SAK-5922 Some password are missing last 4 characters.
91  				if (length == 20) {
92  					passwordEnc = passwordEnc.substring(0, 20);
93  				}
94  				return encrypted.equals(passwordEnc);
95  			}
96  		}
97  		
98  		String passwordMod = password;
99  		String expectedHash = encrypted;
100 		
101 		if (encrypted.startsWith(MD5_SALT_SHA256)) {
102 			passwordMod = hash(password, "MD5");
103 			expectedHash = encrypted.substring(MD5_SALT_SHA256.length());
104 		}
105 		if (encrypted.startsWith(MD5TRUNC_SALT_SHA256)) {
106 			passwordMod = hash(password, "MD5");
107 			if (passwordMod != null && passwordMod.length() > 20) {
108 				passwordMod = passwordMod.substring(0, 20);
109 			}
110 			expectedHash = encrypted.substring(MD5TRUNC_SALT_SHA256.length());
111 		}
112 		int saltDelimPos = expectedHash.indexOf(saltDelim);
113 		String shaSource = passwordMod;
114 		if (saltDelimPos != -1) {
115 			String salt = expectedHash.substring(0, saltDelimPos);
116 			expectedHash = expectedHash.substring(saltDelimPos+1);
117 			shaSource = salt + passwordMod;
118 		}
119 		String passwordEnc = hash(shaSource, defaultAlgorithm);
120 		return expectedHash.equals(passwordEnc);
121 	}
122 	
123 	/**
124 	 * Create a new encrypted password.
125 	 * @param password The password to encrypt.
126 	 * @return The resultant string.
127 	 */
128 	public String encrypt(String password) {
129 		String salt = salt(saltLength);
130 		String source = salt + password;
131 		String hash = hash(source, defaultAlgorithm);
132 		return salt + saltDelim + hash;
133 	}
134 	
135 	/**
136 	 * Digest and Base64 encode the password.
137 	 * @param password The password to hash.
138 	 * @param algorithm The Digest Algorithm to use.
139 	 * @return The digested password or <code>null</code> if it failed.
140 	 */
141 	protected String hash(String password, String algorithm) {
142 		try
143 		{
144 			// compute the digest using the MD5 algorithm
145 			MessageDigest md = MessageDigest.getInstance(algorithm);
146 			byte[] digest = md.digest(password.getBytes("UTF-8"));
147 
148 			// encode as base64
149 			ByteArrayOutputStream bas = new ByteArrayOutputStream(lengthBase64(digest.length));
150 			OutputStream encodedStream = MimeUtility.encode(bas, "base64");
151 			encodedStream.write(digest);
152 			
153 			// close the stream to complete the encoding
154 			encodedStream.close();
155 			String rv = bas.toString().trim(); // '\r\n' is appended by encode()
156 
157 			return rv;
158 		}
159 		catch (Exception e)
160 		{
161 			log.warn("Failed with "+ algorithm, e);
162 			return null;
163 		}
164 	}
165 	
166 	/**
167 	 * Generate a salt which is Base64 encoded.
168 	 * @param length The number of bytes to use for the source.
169 	 * @return A Base64 version of the salt. So it's longer than the source length.
170 	 */
171 	protected String salt(int length) {
172 		try{
173 			byte[] salt = new byte[length];
174 			saltSource.nextBytes(salt);
175 			ByteArrayOutputStream bas = new ByteArrayOutputStream(lengthBase64(length));
176 			OutputStream saltStream;
177 			saltStream = MimeUtility.encode(bas, "base64");
178 			saltStream.write(salt);
179 			saltStream.close();
180 			String rv =  bas.toString().trim(); // '\r\n' is appended by encode() 
181 			return rv;
182 		}catch(Exception e){
183 			log.warn("Failed to generate salt.", e);
184 		}
185 		return "";
186 	}
187 
188 	/**
189 	 * The length needed for base64 encoding some input.
190 	 * http://en.wikipedia.org/wiki/Base64
191 	 * @param length The length in bytes of the source.
192 	 * @return The size in bytes of the output.
193 	 */
194 	private int lengthBase64(int length) {
195 		return (length + 2 - ((length + 2) % 3)) * 4 / 3;
196 	}
197 
198 	public void setSaltLength(int saltLength) {
199 		this.saltLength = saltLength;
200 	}
201 
202 	public void setSaltDelim(String saltDelim) {
203 		this.saltDelim = saltDelim;
204 	}
205 
206 	public void setDefaultAlgorithm(String defaultAlgorithm) {
207 		this.defaultAlgorithm = defaultAlgorithm;
208 	}
209 	
210 }