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    }