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