View Javadoc

1   /**
2    *
3    * Copyright 2003-2005 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.authentication;
19  
20  import java.io.UnsupportedEncodingException;
21  import java.security.MessageDigest;
22  import java.security.NoSuchAlgorithmException;
23  
24  import javax.mail.MessagingException;
25  
26  import org.apache.geronimo.mail.util.Hex;
27  
28  public class CramMD5Authenticator implements ClientAuthenticator {
29  
30      // the user we're authenticating
31      protected String username;
32  
33      // the user's password (the "shared secret")
34      protected String password;
35  
36      // indicates whether we've gone through the entire challenge process.
37      protected boolean complete = false;
38  
39      /**
40       * Main constructor.
41       * 
42       * @param username
43       *            The login user name.
44       * @param password
45       *            The login password.
46       */
47      public CramMD5Authenticator(String username, String password) {
48          this.username = username;
49          this.password = password;
50      }
51  
52      /**
53       * Respond to the hasInitialResponse query. This mechanism does not have an
54       * initial response.
55       * 
56       * @return Always returns false.
57       */
58      public boolean hasInitialResponse() {
59          return false;
60      }
61  
62      /**
63       * Indicate whether the challenge/response process is complete.
64       * 
65       * @return True if the last challenge has been processed, false otherwise.
66       */
67      public boolean isComplete() {
68          return complete;
69      }
70  
71      /**
72       * Retrieve the authenticator mechanism name.
73       * 
74       * @return Always returns the string "CRAM-MD5"
75       */
76      public String getMechanismName() {
77          return "CRAM-MD5";
78      }
79  
80      /**
81       * Evaluate a CRAM-MD5 login challenge, returning the a result string that
82       * should satisfy the clallenge.
83       * 
84       * @param challenge
85       *            The decoded challenge data, as a byte array.
86       * 
87       * @return A formatted challege response, as an array of bytes.
88       * @exception MessagingException
89       */
90      public byte[] evaluateChallenge(byte[] challenge) throws MessagingException {
91          // we create the challenge from the userid and password information (the
92          // "shared secret").
93          byte[] passBytes;
94  
95          try {
96              // get the password in an UTF-8 encoding to create the token
97              passBytes = password.getBytes("UTF-8");
98              // compute the password digest using the key
99              byte[] digest = computeCramDigest(passBytes, challenge);
100 
101             // create a unified string using the user name and the hex encoded
102             // digest
103             String responseString = username + " " + new String(Hex.encode(digest));
104             complete = true;
105             return responseString.getBytes();
106         } catch (UnsupportedEncodingException e) {
107             // got an error, fail this
108             throw new MessagingException("Invalid character encodings");
109         }
110 
111     }
112 
113     /**
114      * Compute a CRAM digest using the hmac_md5 algorithm. See the description
115      * of RFC 2104 for algorithm details.
116      * 
117      * @param key
118      *            The key (K) for the calculation.
119      * @param input
120      *            The encrypted text value.
121      * 
122      * @return The computed digest, as a byte array value.
123      * @exception NoSuchAlgorithmException
124      */
125     protected byte[] computeCramDigest(byte[] key, byte[] input) throws MessagingException {
126         // CRAM digests are computed using the MD5 algorithm.
127         MessageDigest digest;
128         try {
129             digest = MessageDigest.getInstance("MD5");
130         } catch (NoSuchAlgorithmException e) {
131             throw new MessagingException("Unable to access MD5 message digest", e);
132         }
133 
134         // if the key is longer than 64 bytes, then we get a digest of the key
135         // and use that instead.
136         // this is required by RFC 2104.
137         if (key.length > 64) {
138             digest.update(key);
139             key = digest.digest();
140         }
141 
142         // now we create two 64 bit padding keys, initialized with the key
143         // information.
144         byte[] ipad = new byte[64];
145         byte[] opad = new byte[64];
146 
147         System.arraycopy(key, 0, ipad, 0, key.length);
148         System.arraycopy(key, 0, opad, 0, key.length);
149 
150         // and these versions are munged by XORing with "magic" values.
151 
152         for (int i = 0; i < 64; i++) {
153             ipad[i] ^= 0x36;
154             opad[i] ^= 0x5c;
155         }
156 
157         // now there are a pair of MD5 operations performed, and inner and an
158         // outer. The spec defines this as
159         // H(K XOR opad, H(K XOR ipad, text)), where H is the MD5 operation.
160 
161         // inner operation
162         digest.reset();
163         digest.update(ipad);
164         digest.update(input); // this appends the text to the pad
165         byte[] md5digest = digest.digest();
166 
167         // outer operation
168         digest.reset();
169         digest.update(opad);
170         digest.update(md5digest);
171         return digest.digest(); // final result
172     }
173 }