    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                        new String(
    154                            Base64.encodeBase64(("\000" + username + "\000" + password).getBytes())
    155                            )
    156                        );
    157                    if (result == IMAPReply.OK)
    158                    {
    159                        setState(IMAP.IMAPState.AUTH_STATE);
    160                    }
    161                    return result == IMAPReply.OK;
    162                }
    163                case CRAM_MD5:
    164                {
    165                    // get the CRAM challenge (after "+ ")
    166                    byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(2).trim());
    167                    // get the Mac instance
    168                    Mac hmac_md5 = Mac.getInstance("HmacMD5");
    169                    hmac_md5.init(new SecretKeySpec(password.getBytes(), "HmacMD5"));
    170                    // compute the result:
    171                    byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes();
    172                    // join the byte arrays to form the reply
    173                    byte[] usernameBytes = username.getBytes();
    174                    byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
    175                    System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
    176                    toEncode[usernameBytes.length] = ' ';
    177                    System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
    178                    // send the reply and read the server code:
    179                    int result = sendData(new String(Base64.encodeBase64(toEncode)));
    180                    if (result == IMAPReply.OK)
    181                    {
    182                        setState(IMAP.IMAPState.AUTH_STATE);
    183                    }
    184                    return result == IMAPReply.OK;
    185                }
    186                case LOGIN:
    187                {
    188                    // the server sends fixed responses (base64("Username") and
    189                    // base64("Password")), so we don't have to read them.
    190                    if (sendData(
    191                        new String(Base64.encodeBase64(username.getBytes()))) != IMAPReply.CONT)
    192                    {
    193                        return false;
    194                    }
    195                    int result = sendData(
    196                        new String(Base64.encodeBase64(password.getBytes())));
    197                    if (result == IMAPReply.OK)
    198                    {
    199                        setState(IMAP.IMAPState.AUTH_STATE);
    200                    }
    201                    return result == IMAPReply.OK;
    202                }
    203            }
    204            return false; // safety check
    205        }
    206
    207        /**
    208         * Converts the given byte array to a String containing the hex values of the bytes.
    209         * For example, the byte 'A' will be converted to '41', because this is the ASCII code
    210         * (and the byte value) of the capital letter 'A'.
    211         * @param a The byte array to convert.
    212         * @return The resulting String of hex codes.
    213         */
    214        private String _convertToHexString(byte[] a)
    215        {
    216            StringBuilder result = new StringBuilder(a.length*2);
    217            for (int i = 0; i < a.length; i++)
    218            {
    219                if ( (a[i] & 0x0FF) <= 15 ) result.append("0");
    220                result.append(Integer.toHexString(a[i] & 0x0FF));
    221            }
    222            return result.toString();
    223        }
    224
    225        /**
    226         * The enumeration of currently-supported authentication methods.
    227         */
    228        public static enum AUTH_METHOD
    229        {
    230            /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
    231            PLAIN("PLAIN"),
    232            /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
    233            CRAM_MD5("CRAM-MD5"),
    234            /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
    235            LOGIN("LOGIN");
    236
    237            private final String authName;
    238
    239            private AUTH_METHOD(String name){
    240                this.authName=name;
    241            }
    242            /**
    243             * Gets the name of the given authentication method suitable for the server.
    244             * @return The name of the given authentication method suitable for the server.
    245             */
    246            public final String getAuthName()
    247            {
    248                return authName;
    249            }
    250        }
    251    }
    252    /* kate: indent-width 4; replace-tabs on; */