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    import java.security.SecureRandom;
026    import java.util.ArrayList;
027    
028    import javax.mail.AuthenticationFailedException;
029    import javax.mail.MessagingException;
030    
031    import org.apache.geronimo.mail.util.Base64;
032    import org.apache.geronimo.mail.util.Hex;
033    
034    /**
035     * Process a DIGEST-MD5 authentication, using the challenge/response mechanisms.
036     */
037    public class DigestMD5Authenticator implements ClientAuthenticator {
038    
039        protected static final int AUTHENTICATE_CLIENT = 0;
040    
041        protected static final int AUTHENTICATE_SERVER = 1;
042    
043        protected static final int AUTHENTICATION_COMPLETE = 2;
044    
045        // the host server name
046        protected String host;
047    
048        // the user we're authenticating
049        protected String username;
050    
051        // the user's password (the "shared secret")
052        protected String password;
053    
054        // the target login realm
055        protected String realm;
056    
057        // our message digest for processing the challenges.
058        MessageDigest digest;
059    
060        // the string we send to the server on the first challenge.
061        protected String clientResponse;
062    
063        // the response back from an authentication challenge.
064        protected String authenticationResponse = null;
065    
066        // our list of realms received from the server (normally just one).
067        protected ArrayList realms;
068    
069        // the nonce value sent from the server
070        protected String nonce;
071    
072        // indicates whether we've gone through the entire challenge process.
073        protected int stage = AUTHENTICATE_CLIENT;
074    
075        /**
076         * Main constructor.
077         * 
078         * @param host
079         *            The server host name.
080         * @param username
081         *            The login user name.
082         * @param password
083         *            The login password.
084         * @param realm
085         *            The target login realm (can be null).
086         */
087        public DigestMD5Authenticator(String host, String username, String password, String realm) {
088            this.host = host;
089            this.username = username;
090            this.password = password;
091            this.realm = realm;
092        }
093    
094        /**
095         * Respond to the hasInitialResponse query. This mechanism does not have an
096         * initial response.
097         * 
098         * @return Always returns false.
099         */
100        public boolean hasInitialResponse() {
101            return false;
102        }
103    
104        /**
105         * Indicate whether the challenge/response process is complete.
106         * 
107         * @return True if the last challenge has been processed, false otherwise.
108         */
109        public boolean isComplete() {
110            return stage == AUTHENTICATION_COMPLETE;
111        }
112    
113        /**
114         * Retrieve the authenticator mechanism name.
115         * 
116         * @return Always returns the string "DIGEST-MD5"
117         */
118        public String getMechanismName() {
119            return "DIGEST-MD5";
120        }
121    
122        /**
123         * Evaluate a DIGEST-MD5 login challenge, returning the a result string that
124         * should satisfy the clallenge.
125         * 
126         * @param challenge
127         *            The decoded challenge data, as a string.
128         * 
129         * @return A formatted challege response, as an array of bytes.
130         * @exception MessagingException
131         */
132        public byte[] evaluateChallenge(byte[] challenge) throws MessagingException {
133    
134            // DIGEST-MD5 authentication goes in two stages. First state involves us
135            // validating with the
136            // server, the second stage is the server validating with us, using the
137            // shared secret.
138            switch (stage) {
139            // stage one of the process.
140            case AUTHENTICATE_CLIENT: {
141                // get the response and advance the processing stage.
142                byte[] response = authenticateClient(challenge);
143                stage = AUTHENTICATE_SERVER;
144                return response;
145            }
146    
147            // stage two of the process.
148            case AUTHENTICATE_SERVER: {
149                // get the response and advance the processing stage to completed.
150                byte[] response = authenticateServer(challenge);
151                stage = AUTHENTICATION_COMPLETE;
152                return response;
153            }
154    
155            // should never happen.
156            default:
157                throw new MessagingException("Invalid LOGIN challenge");
158            }
159        }
160    
161        /**
162         * Evaluate a DIGEST-MD5 login server authentication challenge, returning
163         * the a result string that should satisfy the clallenge.
164         * 
165         * @param challenge
166         *            The decoded challenge data, as a string.
167         * 
168         * @return A formatted challege response, as an array of bytes.
169         * @exception MessagingException
170         */
171        public byte[] authenticateServer(byte[] challenge) throws MessagingException {
172            // parse the challenge string and validate.
173            if (!parseChallenge(challenge)) {
174                return null;
175            }
176    
177            try {
178                // like all of the client validation steps, the following is order
179                // critical.
180                // first add in the URI information.
181                digest.update((":smtp/" + host).getBytes("US-ASCII"));
182                // now mix in the response we sent originally
183                String responseString = clientResponse + new String(Hex.encode(digest.digest()));
184                digest.update(responseString.getBytes("US-ASCII"));
185    
186                // now convert that into a hex encoded string.
187                String validationText = new String(Hex.encode(digest.digest()));
188    
189                // if everything went well, this calculated value should match what
190                // we got back from the server.
191                // our response back is just a null string....
192                if (validationText.equals(authenticationResponse)) {
193                    return new byte[0];
194                }
195                throw new AuthenticationFailedException("Invalid DIGEST-MD5 response from server");
196            } catch (UnsupportedEncodingException e) {
197                throw new MessagingException("Invalid character encodings");
198            }
199    
200        }
201    
202        /**
203         * Evaluate a DIGEST-MD5 login client authentication challenge, returning
204         * the a result string that should satisfy the clallenge.
205         * 
206         * @param challenge
207         *            The decoded challenge data, as a string.
208         * 
209         * @return A formatted challege response, as an array of bytes.
210         * @exception MessagingException
211         */
212        public byte[] authenticateClient(byte[] challenge) throws MessagingException {
213            // parse the challenge string and validate.
214            if (!parseChallenge(challenge)) {
215                return null;
216            }
217    
218            SecureRandom randomGenerator;
219            // before doing anything, make sure we can get the required crypto
220            // support.
221            try {
222                randomGenerator = new SecureRandom();
223                digest = MessageDigest.getInstance("MD5");
224            } catch (NoSuchAlgorithmException e) {
225                throw new MessagingException("Unable to access cryptography libraries");
226            }
227    
228            // if not configured for a realm, take the first realm from the list, if
229            // any
230            if (realm == null) {
231                // if not handed any realms, just use the host name.
232                if (realms.isEmpty()) {
233                    realm = host;
234                } else {
235                    // pretty arbitrary at this point, so just use the first one.
236                    realm = (String) realms.get(0);
237                }
238            }
239    
240            // use secure random to generate a collection of bytes. that is our
241            // cnonce value.
242            byte[] cnonceBytes = new byte[32];
243    
244            randomGenerator.nextBytes(cnonceBytes);
245            // and get this as a base64 encoded string.
246            String cnonce = new String(Base64.encode(cnonceBytes));
247    
248            // Now the digest computation part. This gets a bit tricky, and must be
249            // done in strict order.
250    
251            try {
252                // this identifies where we're logging into.
253                String idString = username + ":" + realm + ":" + password;
254                // we get a digest for this string, then use the digest for the
255                // first stage
256                // of the next digest operation.
257                digest.update(digest.digest(idString.getBytes("US-ASCII")));
258    
259                // now we add the nonce strings to the digest.
260                String nonceString = ":" + nonce + ":" + cnonce;
261                digest.update(nonceString.getBytes("US-ASCII"));
262    
263                // hex encode this digest, and add on the string values
264                // NB, we only support "auth" for the quality of protection value
265                // (qop). We save this in an
266                // instance variable because we'll need this to validate the
267                // response back from the server.
268                clientResponse = new String(Hex.encode(digest.digest())) + ":" + nonce + ":00000001:" + cnonce + ":auth:";
269    
270                // now we add in identification values to the hash.
271                String authString = "AUTHENTICATE:smtp/" + host;
272                digest.update(authString.getBytes("US-ASCII"));
273    
274                // this gets added on to the client response
275                String responseString = clientResponse + new String(Hex.encode(digest.digest()));
276                // and this gets fed back into the digest
277                digest.update(responseString.getBytes("US-ASCII"));
278    
279                // and FINALLY, the challege digest is hex encoded for sending back
280                // to the server (whew).
281                String challengeResponse = new String(Hex.encode(digest.digest()));
282    
283                // now finally build the keyword/value part of the challenge
284                // response. These can be
285                // in any order.
286                StringBuffer response = new StringBuffer();
287    
288                response.append("username=\"");
289                response.append(username);
290                response.append("\"");
291    
292                response.append(",realm=\"");
293                response.append(realm);
294                response.append("\"");
295    
296                // we only support auth qop values, and the nonce-count (nc) is
297                // always 1.
298                response.append(",qop=auth");
299                response.append(",nc=00000001");
300    
301                response.append(",nonce=\"");
302                response.append(nonce);
303                response.append("\"");
304    
305                response.append(",cnonce=\"");
306                response.append(cnonce);
307                response.append("\"");
308    
309                response.append(",digest-uri=\"smtp/");
310                response.append(host);
311                response.append("\"");
312    
313                response.append(",response=");
314                response.append(challengeResponse);
315    
316                return response.toString().getBytes("US-ASCII");
317    
318            } catch (UnsupportedEncodingException e) {
319                throw new MessagingException("Invalid character encodings");
320            }
321        }
322    
323        /**
324         * Parse the challege string, pulling out information required for our
325         * challenge response.
326         * 
327         * @param challenge
328         *            The challenge data.
329         * 
330         * @return true if there were no errors parsing the string, false otherwise.
331         * @exception MessagingException
332         */
333        protected boolean parseChallenge(byte[] challenge) throws MessagingException {
334            realms = new ArrayList();
335    
336            DigestParser parser = new DigestParser(new String(challenge));
337    
338            // parse the entire string...but we ignore everything but the options we
339            // support.
340            while (parser.hasMore()) {
341                NameValuePair pair = parser.parseNameValuePair();
342    
343                String name = pair.name;
344    
345                // realm to add to our list?
346                if (name.equalsIgnoreCase("realm")) {
347                    realms.add(pair.value);
348                }
349                // we need the nonce to evaluate the client challenge.
350                else if (name.equalsIgnoreCase("nonce")) {
351                    nonce = pair.value;
352                }
353                // rspauth is the challenge replay back, which allows us to validate
354                // that server is also legit.
355                else if (name.equalsIgnoreCase("rspauth")) {
356                    authenticationResponse = pair.value;
357                }
358            }
359    
360            return true;
361        }
362    
363        /**
364         * Inner class for parsing a DIGEST-MD5 challenge string, which is composed
365         * of "name=value" pairs, separated by "," characters.
366         */
367        class DigestParser {
368            // the challenge we're parsing
369            String challenge;
370    
371            // length of the challenge
372            int length;
373    
374            // current parsing position
375            int position;
376    
377            /**
378             * Normal constructor.
379             * 
380             * @param challenge
381             *            The challenge string to be parsed.
382             */
383            public DigestParser(String challenge) {
384                this.challenge = challenge;
385                this.length = challenge.length();
386                position = 0;
387            }
388    
389            /**
390             * Test if there are more values to parse.
391             * 
392             * @return true if we've not reached the end of the challenge string,
393             *         false if the challenge has been completely consumed.
394             */
395            private boolean hasMore() {
396                return position < length;
397            }
398    
399            /**
400             * Return the character at the current parsing position.
401             * 
402             * @return The string character for the current parse position.
403             */
404            private char currentChar() {
405                return challenge.charAt(position);
406            }
407    
408            /**
409             * step forward to the next character position.
410             */
411            private void nextChar() {
412                position++;
413            }
414    
415            /**
416             * Skip over any white space characters in the challenge string.
417             */
418            private void skipSpaces() {
419                while (position < length && Character.isWhitespace(currentChar())) {
420                    position++;
421                }
422            }
423    
424            /**
425             * Parse a quoted string used with a name/value pair, accounting for
426             * escape characters embedded within the string.
427             * 
428             * @return The string value of the character string.
429             */
430            private String parseQuotedValue() {
431                // we're here because we found the starting double quote. Step over
432                // it and parse to the closing
433                // one.
434                nextChar();
435    
436                StringBuffer value = new StringBuffer();
437    
438                while (hasMore()) {
439                    char ch = currentChar();
440    
441                    // is this an escape char?
442                    if (ch == '\\') {
443                        // step past this, and grab the following character
444                        nextChar();
445                        // we have an invalid quoted string....
446                        if (!hasMore()) {
447                            return null;
448                        }
449                        value.append(currentChar());
450                    }
451                    // end of the string?
452                    else if (ch == '"') {
453                        // step over this so the caller doesn't process it.
454                        nextChar();
455                        // return the constructed string.
456                        return value.toString();
457                    } else {
458                        // step over the character and contine with the next
459                        // characteer1
460                        value.append(ch);
461                    }
462                    nextChar();
463                }
464                /* fell off the end without finding a closing quote! */
465                return null;
466            }
467    
468            /**
469             * Parse a token value used with a name/value pair.
470             * 
471             * @return The string value of the token. Returns null if nothing is
472             *         found up to the separater.
473             */
474            private String parseTokenValue() {
475    
476                StringBuffer value = new StringBuffer();
477    
478                while (hasMore()) {
479                    char ch = currentChar();
480                    switch (ch) {
481                    // process the token separators.
482                    case ' ':
483                    case '\t':
484                    case '(':
485                    case ')':
486                    case '<':
487                    case '>':
488                    case '@':
489                    case ',':
490                    case ';':
491                    case ':':
492                    case '\\':
493                    case '"':
494                    case '/':
495                    case '[':
496                    case ']':
497                    case '?':
498                    case '=':
499                    case '{':
500                    case '}':
501                        // no token characters found? this is bad.
502                        if (value.length() == 0) {
503                            return null;
504                        }
505                        // return the accumulated characters.
506                        return value.toString();
507    
508                    default:
509                        // is this a control character? That's a delimiter (likely
510                        // invalid for the next step,
511                        // but it is a token terminator.
512                        if (ch < 32 || ch > 127) {
513                            // no token characters found? this is bad.
514                            if (value.length() == 0) {
515                                return null;
516                            }
517                            // return the accumulated characters.
518                            return value.toString();
519                        }
520                        value.append(ch);
521                        break;
522                    }
523                    // step to the next character.
524                    nextChar();
525                }
526                // no token characters found? this is bad.
527                if (value.length() == 0) {
528                    return null;
529                }
530                // return the accumulated characters.
531                return value.toString();
532            }
533    
534            /**
535             * Parse out a name token of a name/value pair.
536             * 
537             * @return The string value of the name.
538             */
539            private String parseName() {
540                // skip to the value start
541                skipSpaces();
542    
543                // the name is a token.
544                return parseTokenValue();
545            }
546    
547            /**
548             * Parse out a a value of a name/value pair.
549             * 
550             * @return The string value associated with the name.
551             */
552            private String parseValue() {
553                // skip to the value start
554                skipSpaces();
555    
556                // start of a quoted string?
557                if (currentChar() == '"') {
558                    // parse it out as a string.
559                    return parseQuotedValue();
560                }
561                // the value must be a token.
562                return parseTokenValue();
563            }
564    
565            /**
566             * Parse a name/value pair in an DIGEST-MD5 string.
567             * 
568             * @return A NameValuePair object containing the two parts of the value.
569             * @exception MessagingException
570             */
571            public NameValuePair parseNameValuePair() throws MessagingException {
572                // get the name token
573                String name = parseName();
574                if (name == null) {
575                    throw new MessagingException("Name syntax error");
576                }
577    
578                // the name should be followed by an "=" sign
579                if (!hasMore() || currentChar() != '=') {
580                    throw new MessagingException("Name/value pair syntax error");
581                }
582    
583                // step over the equals
584                nextChar();
585    
586                // now get the value part
587                String value = parseValue();
588                if (value == null) {
589                    throw new MessagingException("Name/value pair syntax error");
590                }
591    
592                // skip forward to the terminator, which should either be the end of
593                // the line or a ","
594                skipSpaces();
595                // all that work, only to have a syntax error at the end (sigh)
596                if (hasMore()) {
597                    if (currentChar() != ',') {
598                        throw new MessagingException("Name/value pair syntax error");
599                    }
600                    // step over, and make sure we position ourselves at either the
601                    // end or the first
602                    // real character for parsing the next name/value pair.
603                    nextChar();
604                    skipSpaces();
605                }
606                return new NameValuePair(name, value);
607            }
608        }
609    
610        /**
611         * Simple inner class to represent a name/value pair.
612         */
613        public class NameValuePair {
614            public String name;
615    
616            public String value;
617    
618            NameValuePair(String name, String value) {
619                this.name = name;
620                this.value = value;
621            }
622    
623        }
624    }