001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020 package org.apache.geronimo.javamail.authentication;
021
022 import java.io.UnsupportedEncodingException;
023 import java.security.MessageDigest;
024 import java.security.NoSuchAlgorithmException;
025
026 import javax.mail.MessagingException;
027
028 import org.apache.geronimo.mail.util.Hex;
029
030 public class CramMD5Authenticator implements ClientAuthenticator {
031
032 // the user we're authenticating
033 protected String username;
034
035 // the user's password (the "shared secret")
036 protected String password;
037
038 // indicates whether we've gone through the entire challenge process.
039 protected boolean complete = false;
040
041 /**
042 * Main constructor.
043 *
044 * @param username
045 * The login user name.
046 * @param password
047 * The login password.
048 */
049 public CramMD5Authenticator(String username, String password) {
050 this.username = username;
051 this.password = password;
052 }
053
054 /**
055 * Respond to the hasInitialResponse query. This mechanism does not have an
056 * initial response.
057 *
058 * @return Always returns false.
059 */
060 public boolean hasInitialResponse() {
061 return false;
062 }
063
064 /**
065 * Indicate whether the challenge/response process is complete.
066 *
067 * @return True if the last challenge has been processed, false otherwise.
068 */
069 public boolean isComplete() {
070 return complete;
071 }
072
073 /**
074 * Retrieve the authenticator mechanism name.
075 *
076 * @return Always returns the string "CRAM-MD5"
077 */
078 public String getMechanismName() {
079 return "CRAM-MD5";
080 }
081
082 /**
083 * Evaluate a CRAM-MD5 login challenge, returning the a result string that
084 * should satisfy the clallenge.
085 *
086 * @param challenge
087 * The decoded challenge data, as a byte array.
088 *
089 * @return A formatted challege response, as an array of bytes.
090 * @exception MessagingException
091 */
092 public byte[] evaluateChallenge(byte[] challenge) throws MessagingException {
093 // we create the challenge from the userid and password information (the
094 // "shared secret").
095 byte[] passBytes;
096
097 try {
098 // get the password in an UTF-8 encoding to create the token
099 passBytes = password.getBytes("UTF-8");
100 // compute the password digest using the key
101 byte[] digest = computeCramDigest(passBytes, challenge);
102
103 // create a unified string using the user name and the hex encoded
104 // digest
105 String responseString = username + " " + new String(Hex.encode(digest));
106 complete = true;
107 return responseString.getBytes();
108 } catch (UnsupportedEncodingException e) {
109 // got an error, fail this
110 throw new MessagingException("Invalid character encodings");
111 }
112
113 }
114
115 /**
116 * Compute a CRAM digest using the hmac_md5 algorithm. See the description
117 * of RFC 2104 for algorithm details.
118 *
119 * @param key
120 * The key (K) for the calculation.
121 * @param input
122 * The encrypted text value.
123 *
124 * @return The computed digest, as a byte array value.
125 * @exception NoSuchAlgorithmException
126 */
127 protected byte[] computeCramDigest(byte[] key, byte[] input) throws MessagingException {
128 // CRAM digests are computed using the MD5 algorithm.
129 MessageDigest digest;
130 try {
131 digest = MessageDigest.getInstance("MD5");
132 } catch (NoSuchAlgorithmException e) {
133 throw new MessagingException("Unable to access MD5 message digest", e);
134 }
135
136 // if the key is longer than 64 bytes, then we get a digest of the key
137 // and use that instead.
138 // this is required by RFC 2104.
139 if (key.length > 64) {
140 digest.update(key);
141 key = digest.digest();
142 }
143
144 // now we create two 64 bit padding keys, initialized with the key
145 // information.
146 byte[] ipad = new byte[64];
147 byte[] opad = new byte[64];
148
149 System.arraycopy(key, 0, ipad, 0, key.length);
150 System.arraycopy(key, 0, opad, 0, key.length);
151
152 // and these versions are munged by XORing with "magic" values.
153
154 for (int i = 0; i < 64; i++) {
155 ipad[i] ^= 0x36;
156 opad[i] ^= 0x5c;
157 }
158
159 // now there are a pair of MD5 operations performed, and inner and an
160 // outer. The spec defines this as
161 // H(K XOR opad, H(K XOR ipad, text)), where H is the MD5 operation.
162
163 // inner operation
164 digest.reset();
165 digest.update(ipad);
166 digest.update(input); // this appends the text to the pad
167 byte[] md5digest = digest.digest();
168
169 // outer operation
170 digest.reset();
171 digest.update(opad);
172 digest.update(md5digest);
173 return digest.digest(); // final result
174 }
175 }