    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
    018package org.apache.commons.net.imap;
    019
    020import java.io.IOException;
    021import java.security.InvalidKeyException;
    022import java.security.NoSuchAlgorithmException;
    023import java.security.spec.InvalidKeySpecException;
    024
    025import javax.crypto.Mac;
    026import javax.crypto.spec.SecretKeySpec;
    027import javax.net.ssl.SSLContext;
    028import org.apache.commons.net.util.Base64;
    029
    030/**
    031 * An IMAP Client class with authentication support.
    032 * @see IMAPSClient
    033 */
    034public 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)
    154                            .getBytes(getCharsetName())));  // Java 1.6 can use getCharset()
    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(getCharsetName()), "HmacMD5")); // Java 1.6 can use getCharset()
    168                // compute the result:
    169                byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(getCharsetName()); // Java 1.6 can use getCharset()
    170                // join the byte arrays to form the reply
    171                byte[] usernameBytes = username.getBytes(getCharsetName()); // Java 1.6 can use getCharset()
    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(getCharsetName()))) != IMAPReply.CONT) // Java 1.6 can use getCharset()
    190                {
    191                    return false;
    192                }
    193                int result = sendData(Base64.encodeBase64StringUnChunked(password.getBytes(getCharsetName()))); // Java 1.6 can use getCharset()
    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(username);
    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; */