1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.net.imap;
19
20 import java.io.IOException;
21 import java.security.InvalidKeyException;
22 import java.security.NoSuchAlgorithmException;
23 import java.util.Base64;
24
25 import javax.crypto.Mac;
26 import javax.crypto.spec.SecretKeySpec;
27 import javax.net.ssl.SSLContext;
28
29 /**
30 * An IMAP Client class with authentication support.
31 *
32 * @see IMAPSClient
33 */
34 public class AuthenticatingIMAPClient extends IMAPSClient {
35
36 /**
37 * The enumeration of currently-supported authentication methods.
38 */
39 public enum AUTH_METHOD {
40
41 /** The standardized (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
42
43 PLAIN("PLAIN"),
44
45 /** The standardized (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
46
47 CRAM_MD5("CRAM-MD5"),
48
49 /** The standardized Microsoft LOGIN method, which sends the password unencrypted (insecure). */
50 LOGIN("LOGIN"),
51
52 /** XOAUTH */
53 XOAUTH("XOAUTH"),
54
55 /** XOAUTH 2 */
56 XOAUTH2("XOAUTH2");
57
58 private final String authName;
59
60 AUTH_METHOD(final String name) {
61 this.authName = name;
62 }
63
64 /**
65 * Gets the name of the given authentication method suitable for the server.
66 *
67 * @return The name of the given authentication method suitable for the server.
68 */
69 public String getAuthName() {
70 return authName;
71 }
72 }
73
74 /** {@link Mac} algorithm. */
75 private static final String MAC_ALGORITHM = "HmacMD5";
76
77 /**
78 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient. Sets security mode to explicit (isImplicit = false).
79 */
80 public AuthenticatingIMAPClient() {
81 this(DEFAULT_PROTOCOL, false);
82 }
83
84 /**
85 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
86 *
87 * @param implicit The security mode (Implicit/Explicit).
88 */
89 public AuthenticatingIMAPClient(final boolean implicit) {
90 this(DEFAULT_PROTOCOL, implicit);
91 }
92
93 /**
94 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
95 *
96 * @param implicit The security mode(Implicit/Explicit).
97 * @param ctx A pre-configured SSL Context.
98 */
99 public AuthenticatingIMAPClient(final boolean implicit, final SSLContext ctx) {
100 this(DEFAULT_PROTOCOL, implicit, ctx);
101 }
102
103 /**
104 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
105 *
106 * @param context A pre-configured SSL Context.
107 */
108 public AuthenticatingIMAPClient(final SSLContext context) {
109 this(false, context);
110 }
111
112 /**
113 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
114 *
115 * @param proto the protocol.
116 */
117 public AuthenticatingIMAPClient(final String proto) {
118 this(proto, false);
119 }
120
121 /**
122 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
123 *
124 * @param proto the protocol.
125 * @param implicit The security mode(Implicit/Explicit).
126 */
127 public AuthenticatingIMAPClient(final String proto, final boolean implicit) {
128 this(proto, implicit, null);
129 }
130
131 /**
132 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
133 *
134 * @param proto the protocol.
135 * @param implicit The security mode(Implicit/Explicit).
136 * @param ctx the context
137 */
138 public AuthenticatingIMAPClient(final String proto, final boolean implicit, final SSLContext ctx) {
139 super(proto, implicit, ctx);
140 }
141
142 /**
143 * Authenticate to the IMAP server by sending the AUTHENTICATE command with the selected mechanism, using the given user and the given password.
144 *
145 * @param method the method name
146 * @param user user
147 * @param password password
148 * @return True if successfully completed, false if not.
149 * @throws IOException If an I/O error occurs while either sending a command to the server or receiving a reply from the server.
150 * @throws NoSuchAlgorithmException If the CRAM hash algorithm cannot be instantiated by the Java runtime system.
151 * @throws InvalidKeyException If the CRAM hash algorithm failed to use the given password.
152 */
153 public boolean auth(final AuthenticatingIMAPClient.AUTH_METHOD method, final String user, final String password)
154 throws IOException, NoSuchAlgorithmException, InvalidKeyException {
155 if (!IMAPReply.isContinuation(sendCommand(IMAPCommand.AUTHENTICATE, method.getAuthName()))) {
156 return false;
157 }
158
159 switch (method) {
160 case PLAIN: {
161 // the server sends an empty response ("+ "), so we don't have to read it.
162 final int result = sendData(Base64.getEncoder().encodeToString(("\000" + user + "\000" + password).getBytes(getCharset())));
163 if (result == IMAPReply.OK) {
164 setState(IMAP.IMAPState.AUTH_STATE);
165 }
166 return result == IMAPReply.OK;
167 }
168 case CRAM_MD5: {
169 // get the CRAM challenge (after "+ ")
170 final byte[] serverChallenge = Base64.getDecoder().decode(getReplyString().substring(2).trim());
171 // get the Mac instance
172 final Mac hmacMd5 = Mac.getInstance(MAC_ALGORITHM);
173 hmacMd5.init(new SecretKeySpec(password.getBytes(getCharset()), MAC_ALGORITHM));
174 // compute the result:
175 final byte[] hmacResult = convertToHexString(hmacMd5.doFinal(serverChallenge)).getBytes(getCharset());
176 // join the byte arrays to form the reply
177 final byte[] usernameBytes = user.getBytes(getCharset());
178 final byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
179 System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
180 toEncode[usernameBytes.length] = ' ';
181 System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
182 // send the reply and read the server code:
183 final int result = sendData(Base64.getEncoder().encodeToString(toEncode));
184 if (result == IMAPReply.OK) {
185 setState(IMAP.IMAPState.AUTH_STATE);
186 }
187 return result == IMAPReply.OK;
188 }
189 case LOGIN: {
190 // the server sends fixed responses (base64("UserName") and
191 // base64("Password")), so we don't have to read them.
192 if (sendData(Base64.getEncoder().encodeToString(user.getBytes(getCharset()))) != IMAPReply.CONT) {
193 return false;
194 }
195 final int result = sendData(Base64.getEncoder().encodeToString(password.getBytes(getCharset())));
196 if (result == IMAPReply.OK) {
197 setState(IMAP.IMAPState.AUTH_STATE);
198 }
199 return result == IMAPReply.OK;
200 }
201 case XOAUTH:
202 case XOAUTH2: {
203 final int result = sendData(user);
204 if (result == IMAPReply.OK) {
205 setState(IMAP.IMAPState.AUTH_STATE);
206 }
207 return result == IMAPReply.OK;
208 }
209 }
210 return false; // safety check
211 }
212
213 /**
214 * Authenticate to the IMAP server by sending the AUTHENTICATE command with the selected mechanism, using the given user and the given password.
215 *
216 * @param method the method name
217 * @param user user
218 * @param password password
219 * @return True if successfully completed, false if not.
220 * @throws IOException If an I/O error occurs while either sending a command to the server or receiving a reply from the server.
221 * @throws NoSuchAlgorithmException If the CRAM hash algorithm cannot be instantiated by the Java runtime system.
222 * @throws InvalidKeyException If the CRAM hash algorithm failed to use the given password.
223 */
224 public boolean authenticate(final AuthenticatingIMAPClient.AUTH_METHOD method, final String user, final String password)
225 throws IOException, NoSuchAlgorithmException, InvalidKeyException {
226 return auth(method, user, password);
227 }
228
229 /**
230 * 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
231 * this is the ASCII code (and the byte value) of the capital letter 'A'.
232 *
233 * @param a The byte array to convert.
234 * @return The resulting String of hexadecimal codes.
235 */
236 private String convertToHexString(final byte[] a) {
237 final StringBuilder result = new StringBuilder(a.length * 2);
238 for (final byte element : a) {
239 if ((element & 0x0FF) <= 15) {
240 result.append("0");
241 }
242 result.append(Integer.toHexString(element & 0x0FF));
243 }
244 return result.toString();
245 }
246 }
247