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