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 /** The standardized (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
45
46 CRAM_MD5("CRAM-MD5"),
47
48 /** The standardized Microsoft LOGIN method, which sends the password unencrypted (insecure). */
49 LOGIN("LOGIN"),
50
51 /** XOAUTH */
52 XOAUTH("XOAUTH"),
53
54 /** XOAUTH 2 */
55 XOAUTH2("XOAUTH2");
56
57 private final String authName;
58
59 AUTH_METHOD(final String name) {
60 this.authName = name;
61 }
62
63 /**
64 * Gets the name of the given authentication method suitable for the server.
65 *
66 * @return The name of the given authentication method suitable for the server.
67 */
68 public String getAuthName() {
69 return authName;
70 }
71 }
72
73 /** {@link Mac} algorithm. */
74 private static final String MAC_ALGORITHM = "HmacMD5";
75
76 /**
77 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient. Sets security mode to explicit (isImplicit = false).
78 */
79 public AuthenticatingIMAPClient() {
80 this(DEFAULT_PROTOCOL, false);
81 }
82
83 /**
84 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
85 *
86 * @param implicit The security mode (Implicit/Explicit).
87 */
88 public AuthenticatingIMAPClient(final boolean implicit) {
89 this(DEFAULT_PROTOCOL, implicit);
90 }
91
92 /**
93 * Constructor for AuthenticatingIMAPClient that delegates to IMAPSClient.
94 *
95 * @param implicit The security mode(Implicit/Explicit).
96 * @param ctx A pre-configured SSL Context.
97 */
98 public AuthenticatingIMAPClient(final boolean implicit, final SSLContext ctx) {
99 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