001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.server.security;
031:
032: import com.caucho.config.ConfigException;
033: import com.caucho.util.Base64;
034: import com.caucho.util.CharBuffer;
035: import com.caucho.util.L10N;
036:
037: import javax.annotation.PostConstruct;
038: import javax.servlet.ServletContext;
039: import javax.servlet.ServletException;
040: import javax.servlet.http.HttpServletRequest;
041: import javax.servlet.http.HttpServletResponse;
042: import java.security.MessageDigest;
043:
044: /**
045: * Calculates a digest for the user and password.
046: *
047: * <p>If the realm is missing, the digest will calculate:
048: * <code><pre>
049: * MD5(user + ':' + password)
050: * </pre></code>
051: *
052: * <p>If the realm is specified, the digest will calculate:
053: * <code><pre>
054: * MD5(user + ':' + realm + ':' + password)
055: * </pre></code>
056: *
057: * <p>The second version matches the way HTTP digest authentication
058: * is handled, so it is the preferred method for storing passwords.
059: *
060: * <p>The returned result is the base64 encoding of the digest.
061: */
062: public class PasswordDigest {
063: private final L10N L = new L10N(PasswordDigest.class);
064:
065: private String _algorithm = "MD5";
066: private String _format = "base64";
067: private String _realm = null;
068: private MessageDigest _digest;
069: private byte[] _digestBytes = new byte[256];
070: private boolean _isOldEncoding;
071:
072: /**
073: * Returns the message digest algorithm.
074: */
075: public void setAlgorithm(String algorithm) {
076: _algorithm = algorithm;
077: }
078:
079: /**
080: * Returns the message digest algorithm.
081: */
082: public String getAlgorithm() {
083: return _algorithm;
084: }
085:
086: /**
087: * Set the message digest format (base64 or hex).
088: */
089: public void setFormat(String format) {
090: _format = format;
091: }
092:
093: /**
094: * Returns the message digest format (base64 or hex).
095: */
096: public String getFormat() {
097: return _format;
098: }
099:
100: /**
101: * Set the message digest default realm
102: */
103: public void setRealm(String realm) {
104: _realm = realm;
105: }
106:
107: /**
108: * Returns the message digest default realm.
109: */
110: public String getRealm() {
111: return _realm;
112: }
113:
114: /**
115: * Sets true for the old, buggy encoding.
116: */
117: public void setOldEncoding(boolean isOldEncoding) {
118: _isOldEncoding = isOldEncoding;
119: }
120:
121: /**
122: * Sets the algorithm for bean-style init.
123: */
124: public void addText(String value) throws ConfigException {
125: int p = value.indexOf('-');
126:
127: if (p > 0) {
128: String algorithm = value.substring(0, p);
129: String format = value.substring(p + 1);
130:
131: setAlgorithm(algorithm);
132: setFormat(format);
133: } else if (value.equals("none")) {
134: setAlgorithm(null);
135: setAlgorithm(null);
136: } else
137: throw new ConfigException(
138: L
139: .l(
140: "{0} is an illegal algorithm. Expected `none' or `alogorithm-format', for example `MD5-base64'.",
141: value));
142: }
143:
144: /**
145: * Initialize the digest.
146: */
147: @PostConstruct
148: synchronized public void init() throws ServletException {
149: if (_algorithm == null)
150: return;
151:
152: try {
153: _digest = MessageDigest.getInstance(_algorithm);
154: } catch (Exception e) {
155: throw new ServletException(e);
156: }
157: }
158:
159: /**
160: * Returns the digest of the password
161: */
162: public String getPasswordDigest(String password)
163: throws ServletException {
164: return getPasswordDigest(null, null, null, password, _realm);
165: }
166:
167: /**
168: * Returns the digest of the user/password
169: */
170: public String getPasswordDigest(String user, String password)
171: throws ServletException {
172: return getPasswordDigest(null, null, null, user, password,
173: _realm);
174: }
175:
176: /**
177: * Returns the digest of the user/password
178: */
179: public String getPasswordDigest(String user, String password,
180: String realm) throws ServletException {
181: return getPasswordDigest(null, null, null, user, password,
182: realm);
183: }
184:
185: /**
186: * Returns the digest of the user/password
187: *
188: * <p>The default implementation returns the digest of
189: * <b>user:password</b> or <b>user:realm:password</b> if a
190: * default realm has been configured.
191: *
192: * @param request the http request
193: * @param response the http response
194: * @param app the servlet context
195: * @param user the user name
196: * @param password the cleartext password
197: */
198: public String getPasswordDigest(HttpServletRequest request,
199: HttpServletResponse response, ServletContext app,
200: String user, String password) throws ServletException {
201: return getPasswordDigest(request, response, app, user,
202: password, _realm);
203: }
204:
205: /**
206: * Returns the digest of the user/password
207: *
208: * <p>The default implementation returns the digest of
209: * <b>user:realm:password</b>. If the realm is null, it will use
210: * <b>user:password</b>.
211: *
212: * @param request the http request
213: * @param response the http response
214: * @param app the servlet context
215: * @param user the user name
216: * @param password the cleartext password
217: * @param realm the security realm
218: */
219: public String getPasswordDigest(HttpServletRequest request,
220: HttpServletResponse response, ServletContext app,
221: String user, String password, String realm)
222: throws ServletException {
223: if (_digest == null)
224: init();
225:
226: try {
227: synchronized (_digest) {
228: _digest.reset();
229:
230: updateDigest(_digest, user, password, realm);
231:
232: int len = _digest.digest(_digestBytes, 0,
233: _digestBytes.length);
234:
235: return digestToString(_digestBytes, len);
236: }
237: } catch (Exception e) {
238: throw new ServletException(e);
239: }
240: }
241:
242: /**
243: * Updates the digest based on the user:realm:password
244: */
245: protected void updateDigest(MessageDigest digest, String user,
246: String password, String realm) {
247: if (user != null) {
248: addDigestUTF8(digest, user);
249: digest.update((byte) ':');
250: }
251:
252: if (realm != null && !realm.equals("none")) {
253: addDigestUTF8(digest, realm);
254: digest.update((byte) ':');
255: }
256:
257: addDigestUTF8(digest, password);
258: }
259:
260: /**
261: * Adds the string to the digest using a UTF8 encoding.
262: */
263: protected static void addDigestUTF8(MessageDigest digest,
264: String string) {
265: if (string == null)
266: return;
267:
268: int len = string.length();
269: for (int i = 0; i < len; i++) {
270: int ch = string.charAt(i);
271: if (ch < 0x80)
272: digest.update((byte) ch);
273: else if (ch < 0x800) {
274: digest.update((byte) (0xc0 + (ch >> 6)));
275: digest.update((byte) (0x80 + (ch & 0x3f)));
276: } else {
277: digest.update((byte) (0xe0 + (ch >> 12)));
278: digest.update((byte) (0x80 + ((ch >> 6) & 0x3f)));
279: digest.update((byte) (0x80 + (ch & 0x3f)));
280: }
281: }
282: }
283:
284: /**
285: * Convert the string to a digest byte array.
286: */
287: protected byte[] stringToDigest(String s) {
288: return Base64.decodeToByteArray(s);
289: }
290:
291: /**
292: * Convert the digest byte array to a string.
293: */
294: protected String digestToString(byte[] digest, int len) {
295: if (!_format.equals("base64"))
296: return digestToHex(digest, len);
297: else if (_isOldEncoding)
298: return digestToOldBase64(digest, len);
299: else
300: return digestToBase64(digest, len);
301: }
302:
303: protected static String digestToBase64(byte[] digest, int len) {
304: CharBuffer cb = CharBuffer.allocate();
305:
306: Base64.encode(cb, digest, 0, len);
307:
308: return cb.close();
309: }
310:
311: protected static String digestToOldBase64(byte[] digest, int len) {
312: CharBuffer cb = CharBuffer.allocate();
313:
314: Base64.oldEncode(cb, digest, 0, len);
315:
316: return cb.close();
317: }
318:
319: protected static String digestToHex(byte[] digest, int len) {
320: CharBuffer cb = CharBuffer.allocate();
321:
322: for (int i = 0; i < len; i++) {
323: int d1 = (digest[i] >> 4) & 0xf;
324: int d2 = digest[i] & 0xf;
325:
326: if (d1 >= 10)
327: cb.append((char) (d1 + 'a' - 10));
328: else
329: cb.append((char) (d1 + '0'));
330:
331: if (d2 >= 10)
332: cb.append((char) (d2 + 'a' - 10));
333: else
334: cb.append((char) (d2 + '0'));
335: }
336:
337: return cb.close();
338: }
339:
340: public String toString() {
341: return getClass().getSimpleName() + "[]";
342: }
343: }
|