Package: DigestMD5
DigestMD5
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
DigestMD5(MailLogger) |
|
|
|
|
|
||||||||||||||||||||
authClient(String, String, String, String, String) |
|
|
|
|
|
||||||||||||||||||||
authServer(String) |
|
|
|
|
|
||||||||||||||||||||
static {...} |
|
|
|
|
|
||||||||||||||||||||
toHex(byte[]) |
|
|
|
|
|
||||||||||||||||||||
tokenize(String) |
|
|
|
|
|
Coverage
1: /*
2: * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
3: *
4: * This program and the accompanying materials are made available under the
5: * terms of the Eclipse Public License v. 2.0, which is available at
6: * http://www.eclipse.org/legal/epl-2.0.
7: *
8: * This Source Code may also be made available under the following Secondary
9: * Licenses when the conditions for such availability set forth in the
10: * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11: * version 2 with the GNU Classpath Exception, which is available at
12: * https://www.gnu.org/software/classpath/license.html.
13: *
14: * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15: */
16:
17: package org.eclipse.angus.mail.smtp;
18:
19: import java.io.ByteArrayInputStream;
20: import java.io.ByteArrayOutputStream;
21: import java.io.IOException;
22: import java.io.InputStreamReader;
23: import java.io.OutputStream;
24: import java.io.StreamTokenizer;
25: import java.nio.charset.StandardCharsets;
26: import java.security.MessageDigest;
27: import java.security.NoSuchAlgorithmException;
28: import java.security.SecureRandom;
29: import java.util.HashMap;
30: import java.util.Map;
31: import java.util.StringTokenizer;
32: import java.util.logging.Level;
33:
34: import org.eclipse.angus.mail.util.BASE64DecoderStream;
35: import org.eclipse.angus.mail.util.BASE64EncoderStream;
36: import org.eclipse.angus.mail.util.ASCIIUtility;
37: import org.eclipse.angus.mail.util.MailLogger;
38:
39: /**
40: * DIGEST-MD5 authentication support.
41: *
42: * @author Dean Gibson
43: * @author Bill Shannon
44: */
45:
46: public class DigestMD5 {
47:
48: private MailLogger logger;
49: private MessageDigest md5;
50: private String uri;
51: private String clientResponse;
52:
53: public DigestMD5(MailLogger logger) {
54:         this.logger = logger.getLogger(this.getClass(), "DEBUG DIGEST-MD5");
55:         logger.config("DIGEST-MD5 Loaded");
56: }
57:
58: /**
59: * Return client's authentication response to server's challenge.
60: *
61: * @param        host        the host name
62: * @param        user        the user name
63: * @param        passwd        the user's password
64: * @param        realm        the security realm
65: * @param        serverChallenge        the challenge from the server
66: * @return byte array with client's response
67: * @exception        IOException        for I/O errors
68: */
69: public byte[] authClient(String host, String user, String passwd,
70:                                 String realm, String serverChallenge)
71:                                 throws IOException {
72:         ByteArrayOutputStream bos = new ByteArrayOutputStream();
73:         OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);
74:         SecureRandom random;
75:         try {
76:          //random = SecureRandom.getInstance("SHA1PRNG");
77:          random = new SecureRandom();
78:          md5 = MessageDigest.getInstance("MD5");
79:         } catch (NoSuchAlgorithmException ex) {
80:          logger.log(Level.FINE, "NoSuchAlgorithmException", ex);
81:          throw new IOException(ex.toString());
82:         }
83:         StringBuilder result = new StringBuilder();
84:
85:         uri = "smtp/" + host;
86:         String nc = "00000001";
87:         String qop = "auth";
88:         byte[] bytes = new byte[32];        // arbitrary size ...
89:
90:         logger.fine("Begin authentication ...");
91:
92:         // Code based on http://www.ietf.org/rfc/rfc2831.txt
93:         Map<String, String> map = tokenize(serverChallenge);
94:
95:•        if (realm == null) {
96:          String text = map.get("realm");
97:•         realm = text != null ? new StringTokenizer(text, ",").nextToken()
98:                                  : host;
99:         }
100:
101:         // server challenge random value
102:         String nonce = map.get("nonce");
103:
104:         // Does server support UTF-8 usernames and passwords?
105:         String charset = map.get("charset");
106:•        boolean utf8 = charset != null && charset.equalsIgnoreCase("utf-8");
107:
108:         random.nextBytes(bytes);
109:         b64os.write(bytes);
110:         b64os.flush();
111:
112:         // client challenge random value
113:         String cnonce = bos.toString("iso-8859-1");        // really ASCII?
114:         bos.reset();
115:
116:         // DIGEST-MD5 computation, common portion (order critical)
117:•        if (utf8) {
118:          String up = user + ":" + realm + ":" + passwd;
119:          md5.update(md5.digest(up.getBytes(StandardCharsets.UTF_8)));
120:         } else
121:          md5.update(md5.digest(
122:                 ASCIIUtility.getBytes(user + ":" + realm + ":" + passwd)));
123:         md5.update(ASCIIUtility.getBytes(":" + nonce + ":" + cnonce));
124:         clientResponse = toHex(md5.digest())
125:                 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":";
126:         
127:         // DIGEST-MD5 computation, client response (order critical)
128:         md5.update(ASCIIUtility.getBytes("AUTHENTICATE:" + uri));
129:         md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest())));
130:
131:         // build response text (order not critical)
132:         result.append("username=\"" + user + "\"");
133:         result.append(",realm=\"" + realm + "\"");
134:         result.append(",qop=" + qop);
135:         result.append(",nc=" + nc);
136:         result.append(",nonce=\"" + nonce + "\"");
137:         result.append(",cnonce=\"" + cnonce + "\"");
138:         result.append(",digest-uri=\"" + uri + "\"");
139:•        if (utf8)
140:          result.append(",charset=\"utf-8\"");
141:         result.append(",response=" + toHex(md5.digest()));
142:
143:•        if (logger.isLoggable(Level.FINE))
144:          logger.fine("Response => " + result.toString());
145:         b64os.write(ASCIIUtility.getBytes(result.toString()));
146:         b64os.flush();
147:         return bos.toByteArray();
148: }
149:
150: /**
151: * Allow the client to authenticate the server based on its
152: * response.
153: *
154: * @param        serverResponse        the response that was received from the server
155: * @return        true if server is authenticated
156: * @exception        IOException        for character conversion failures
157: */
158: public boolean authServer(String serverResponse) throws IOException {
159:         Map<String, String> map = tokenize(serverResponse);
160:         // DIGEST-MD5 computation, server response (order critical)
161:         md5.update(ASCIIUtility.getBytes(":" + uri));
162:         md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest())));
163:         String text = toHex(md5.digest());
164:•        if (!text.equals(map.get("rspauth"))) {
165:•         if (logger.isLoggable(Level.FINE))
166:                 logger.fine("Expected => rspauth=" + text);
167:          return false;        // server NOT authenticated by client !!!
168:         }
169:         return true;
170: }
171:
172: /**
173: * Tokenize a response from the server.
174: *
175: * @return        Map containing key/value pairs from server
176: */
177: @SuppressWarnings("fallthrough")
178: private Map<String, String> tokenize(String serverResponse)
179:          throws IOException {
180:         Map<String, String> map        = new HashMap<>();
181:         byte[] bytes = serverResponse.getBytes("iso-8859-1");        // really ASCII?
182:         String key = null;
183:         int ttype;
184:         StreamTokenizer        tokens
185:                 = new StreamTokenizer(
186:                  new InputStreamReader(
187:                  new BASE64DecoderStream(
188:                         new ByteArrayInputStream(bytes, 4, bytes.length - 4)
189:                  ), "iso-8859-1"        // really ASCII?
190:                  )
191:                  );
192:
193:         tokens.ordinaryChars('0', '9');        // reset digits
194:         tokens.wordChars('0', '9');        // digits may start words
195:•        while ((ttype = tokens.nextToken()) != StreamTokenizer.TT_EOF) {
196:•         switch (ttype) {
197:          case StreamTokenizer.TT_WORD:
198:•                if (key == null) {
199:                  key = tokens.sval;
200:                  break;
201:                 }
202:                 // fall-thru
203:          case '"':
204:•                if (logger.isLoggable(Level.FINE))
205:                  logger.fine("Received => " +
206:                                   key + "='" + tokens.sval + "'");
207:•                if (map.containsKey(key)) { // concatenate multiple values
208:                  map.put(key, map.get(key) + "," + tokens.sval);
209:                 } else {
210:                  map.put(key, tokens.sval);
211:                 }
212:                 key = null;
213:                 break;
214:          default:        // XXX - should never happen?
215:                 break;
216:          }
217:         }
218:         return map;
219: }
220:
221: private static char[] digits = {
222:         '0', '1', '2', '3', '4', '5', '6', '7',
223:         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
224: };
225:
226: /**
227: * Convert a byte array to a string of hex digits representing the bytes.
228: */
229: private static String toHex(byte[] bytes) {
230:         char[] result = new char[bytes.length * 2];
231:
232:•        for (int index = 0, i = 0; index < bytes.length; index++) {
233:          int temp = bytes[index] & 0xFF;
234:          result[i++] = digits[temp >> 4];
235:          result[i++] = digits[temp & 0xF];
236:         }
237:         return new String(result);
238: }
239: }