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