001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.commons.net.imap;
019    
020    import java.io.IOException;
021    import java.security.InvalidKeyException;
022    import java.security.NoSuchAlgorithmException;
023    import java.security.spec.InvalidKeySpecException;
024    
025    import javax.crypto.Mac;
026    import javax.crypto.spec.SecretKeySpec;
027    import javax.net.ssl.SSLContext;
028    import org.apache.commons.net.util.Base64;
029    
030    /**
031     * An IMAP Client class with authentication support.
032     * @see IMAPSClient
033     */
034    public class AuthenticatingIMAPClient extends IMAPSClient
035    {
036        /**
037         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
038         * Sets security mode to explicit (isImplicit = false).
039         */
040        public AuthenticatingIMAPClient()
041        {
042            this(DEFAULT_PROTOCOL, false);
043        }
044    
045        /**
046         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
047         * @param implicit The security mode (Implicit/Explicit).
048         */
049        public AuthenticatingIMAPClient(boolean implicit)
050        {
051            this(DEFAULT_PROTOCOL, implicit);
052        }
053    
054        /**
055         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
056         * @param proto the protocol.
057         */
058        public AuthenticatingIMAPClient(String proto)
059        {
060            this(proto, false);
061        }
062    
063        /**
064         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
065         * @param proto the protocol.
066         * @param implicit The security mode(Implicit/Explicit).
067         */
068        public AuthenticatingIMAPClient(String proto, boolean implicit)
069        {
070            this(proto, implicit, null);
071        }
072    
073        /**
074         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
075         * @param proto the protocol.
076         * @param implicit The security mode(Implicit/Explicit).
077         */
078        public AuthenticatingIMAPClient(String proto, boolean implicit, SSLContext ctx)
079        {
080            super(proto, implicit, ctx);
081        }
082    
083        /**
084         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
085         * @param implicit The security mode(Implicit/Explicit).
086         * @param ctx A pre-configured SSL Context.
087         */
088        public AuthenticatingIMAPClient(boolean implicit, SSLContext ctx)
089        {
090            this(DEFAULT_PROTOCOL, implicit, ctx);
091        }
092    
093        /**
094         * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
095         * @param context A pre-configured SSL Context.
096         */
097        public AuthenticatingIMAPClient(SSLContext context)
098        {
099            this(false, context);
100        }
101    
102        /**
103         * Authenticate to the IMAP server by sending the AUTHENTICATE command with the
104         * selected mechanism, using the given username and the given password.
105         * <p>
106         * @return True if successfully completed, false if not.
107         * @exception IOException  If an I/O error occurs while either sending a
108         *      command to the server or receiving a reply from the server.
109         * @exception NoSuchAlgorithmException If the CRAM hash algorithm
110         *      cannot be instantiated by the Java runtime system.
111         * @exception InvalidKeyException If the CRAM hash algorithm
112         *      failed to use the given password.
113         * @exception InvalidKeySpecException If the CRAM hash algorithm
114         *      failed to use the given password.
115         */
116        public boolean authenticate(AuthenticatingIMAPClient.AUTH_METHOD method,
117                            String username, String password)
118                            throws IOException, NoSuchAlgorithmException,
119                            InvalidKeyException, InvalidKeySpecException
120        {
121            return auth(method, username, password);
122        }
123    
124        /**
125         * Authenticate to the IMAP server by sending the AUTHENTICATE command with the
126         * selected mechanism, using the given username and the given password.
127         * <p>
128         * @return True if successfully completed, false if not.
129         * @exception IOException  If an I/O error occurs while either sending a
130         *      command to the server or receiving a reply from the server.
131         * @exception NoSuchAlgorithmException If the CRAM hash algorithm
132         *      cannot be instantiated by the Java runtime system.
133         * @exception InvalidKeyException If the CRAM hash algorithm
134         *      failed to use the given password.
135         * @exception InvalidKeySpecException If the CRAM hash algorithm
136         *      failed to use the given password.
137         */
138        public boolean auth(AuthenticatingIMAPClient.AUTH_METHOD method,
139                            String username, String password)
140                            throws IOException, NoSuchAlgorithmException,
141                            InvalidKeyException, InvalidKeySpecException
142        {
143            if (!IMAPReply.isContinuation(sendCommand(IMAPCommand.AUTHENTICATE, method.getAuthName())))
144            {
145                return false;
146            }
147    
148            switch (method) {
149                case PLAIN:
150                {
151                    // the server sends an empty response ("+ "), so we don't have to read it.
152                    int result = sendData(
153                        Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes())
154                        );
155                    if (result == IMAPReply.OK)
156                    {
157                        setState(IMAP.IMAPState.AUTH_STATE);
158                    }
159                    return result == IMAPReply.OK;
160                }
161                case CRAM_MD5:
162                {
163                    // get the CRAM challenge (after "+ ")
164                    byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(2).trim());
165                    // get the Mac instance
166                    Mac hmac_md5 = Mac.getInstance("HmacMD5");
167                    hmac_md5.init(new SecretKeySpec(password.getBytes(), "HmacMD5"));
168                    // compute the result:
169                    byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes();
170                    // join the byte arrays to form the reply
171                    byte[] usernameBytes = username.getBytes();
172                    byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
173                    System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
174                    toEncode[usernameBytes.length] = ' ';
175                    System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
176                    // send the reply and read the server code:
177                    int result = sendData(Base64.encodeBase64StringUnChunked(toEncode));
178                    if (result == IMAPReply.OK)
179                    {
180                        setState(IMAP.IMAPState.AUTH_STATE);
181                    }
182                    return result == IMAPReply.OK;
183                }
184                case LOGIN:
185                {
186                    // the server sends fixed responses (base64("Username") and
187                    // base64("Password")), so we don't have to read them.
188                    if (sendData(
189                        Base64.encodeBase64StringUnChunked(username.getBytes())) != IMAPReply.CONT)
190                    {
191                        return false;
192                    }
193                    int result = sendData(Base64.encodeBase64StringUnChunked(password.getBytes()));
194                    if (result == IMAPReply.OK)
195                    {
196                        setState(IMAP.IMAPState.AUTH_STATE);
197                    }
198                    return result == IMAPReply.OK;
199                }
200                case XOAUTH:
201                {
202                    int result = sendData(new String(username.getBytes()));
203                    if (result == IMAPReply.OK)
204                    {
205                        setState(IMAP.IMAPState.AUTH_STATE);
206                    }
207                    return result == IMAPReply.OK;
208                }
209            }
210            return false; // safety check
211        }
212    
213        /**
214         * Converts the given byte array to a String containing the hex values of the bytes.
215         * For example, the byte 'A' will be converted to '41', because this is the ASCII code
216         * (and the byte value) of the capital letter 'A'.
217         * @param a The byte array to convert.
218         * @return The resulting String of hex codes.
219         */
220        private String _convertToHexString(byte[] a)
221        {
222            StringBuilder result = new StringBuilder(a.length*2);
223            for (byte element : a)
224            {
225                if ( (element & 0x0FF) <= 15 ) {
226                    result.append("0");
227                }
228                result.append(Integer.toHexString(element & 0x0FF));
229            }
230            return result.toString();
231        }
232    
233        /**
234         * The enumeration of currently-supported authentication methods.
235         */
236        public static enum AUTH_METHOD
237        {
238            /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
239            PLAIN("PLAIN"),
240            /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
241            CRAM_MD5("CRAM-MD5"),
242            /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
243            LOGIN("LOGIN"),
244            /** XOAUTH */
245            XOAUTH("XOAUTH");
246            
247            private final String authName;
248            
249            private AUTH_METHOD(String name){
250                this.authName=name;
251            }
252            /**
253             * Gets the name of the given authentication method suitable for the server.
254             * @return The name of the given authentication method suitable for the server.
255             */
256            public final String getAuthName()
257            {
258                return authName;
259            }
260        }
261    }
262    /* kate: indent-width 4; replace-tabs on; */