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