1 /**
2 *
3 * Copyright 2003-2005 The Apache Software Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://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.geronimo.javamail.authentication;
19
20 import java.io.UnsupportedEncodingException;
21 import java.security.MessageDigest;
22 import java.security.NoSuchAlgorithmException;
23 import java.security.SecureRandom;
24 import java.util.ArrayList;
25
26 import javax.mail.AuthenticationFailedException;
27 import javax.mail.MessagingException;
28
29 import org.apache.geronimo.mail.util.Base64;
30 import org.apache.geronimo.mail.util.Hex;
31
32 /**
33 * Process a DIGEST-MD5 authentication, using the challenge/response mechanisms.
34 */
35 public class DigestMD5Authenticator implements ClientAuthenticator {
36
37 protected static final int AUTHENTICATE_CLIENT = 0;
38
39 protected static final int AUTHENTICATE_SERVER = 1;
40
41 protected static final int AUTHENTICATION_COMPLETE = 2;
42
43
44 protected String host;
45
46
47 protected String username;
48
49
50 protected String password;
51
52
53 protected String realm;
54
55
56 MessageDigest digest;
57
58
59 protected String clientResponse;
60
61
62 protected String authenticationResponse = null;
63
64
65 protected ArrayList realms;
66
67
68 protected String nonce;
69
70
71 protected int stage = AUTHENTICATE_CLIENT;
72
73 /**
74 * Main constructor.
75 *
76 * @param host
77 * The server host name.
78 * @param username
79 * The login user name.
80 * @param password
81 * The login password.
82 * @param realm
83 * The target login realm (can be null).
84 */
85 public DigestMD5Authenticator(String host, String username, String password, String realm) {
86 this.host = host;
87 this.username = username;
88 this.password = password;
89 this.realm = realm;
90 }
91
92 /**
93 * Respond to the hasInitialResponse query. This mechanism does not have an
94 * initial response.
95 *
96 * @return Always returns false.
97 */
98 public boolean hasInitialResponse() {
99 return false;
100 }
101
102 /**
103 * Indicate whether the challenge/response process is complete.
104 *
105 * @return True if the last challenge has been processed, false otherwise.
106 */
107 public boolean isComplete() {
108 return stage == AUTHENTICATION_COMPLETE;
109 }
110
111 /**
112 * Retrieve the authenticator mechanism name.
113 *
114 * @return Always returns the string "DIGEST-MD5"
115 */
116 public String getMechanismName() {
117 return "DIGEST-MD5";
118 }
119
120 /**
121 * Evaluate a DIGEST-MD5 login challenge, returning the a result string that
122 * should satisfy the clallenge.
123 *
124 * @param challenge
125 * The decoded challenge data, as a string.
126 *
127 * @return A formatted challege response, as an array of bytes.
128 * @exception MessagingException
129 */
130 public byte[] evaluateChallenge(byte[] challenge) throws MessagingException {
131
132
133
134
135
136 switch (stage) {
137
138 case AUTHENTICATE_CLIENT: {
139
140 byte[] response = authenticateClient(challenge);
141 stage = AUTHENTICATE_SERVER;
142 return response;
143 }
144
145
146 case AUTHENTICATE_SERVER: {
147
148 byte[] response = authenticateServer(challenge);
149 stage = AUTHENTICATION_COMPLETE;
150 return response;
151 }
152
153
154 default:
155 throw new MessagingException("Invalid LOGIN challenge");
156 }
157 }
158
159 /**
160 * Evaluate a DIGEST-MD5 login server authentication challenge, returning
161 * the a result string that should satisfy the clallenge.
162 *
163 * @param challenge
164 * The decoded challenge data, as a string.
165 *
166 * @return A formatted challege response, as an array of bytes.
167 * @exception MessagingException
168 */
169 public byte[] authenticateServer(byte[] challenge) throws MessagingException {
170
171 if (!parseChallenge(challenge)) {
172 return null;
173 }
174
175 try {
176
177
178
179 digest.update((":smtp/" + host).getBytes("US-ASCII"));
180
181 String responseString = clientResponse + new String(Hex.encode(digest.digest()));
182 digest.update(responseString.getBytes("US-ASCII"));
183
184
185 String validationText = new String(Hex.encode(digest.digest()));
186
187
188
189
190 if (validationText.equals(authenticationResponse)) {
191 return new byte[0];
192 }
193 throw new AuthenticationFailedException("Invalid DIGEST-MD5 response from server");
194 } catch (UnsupportedEncodingException e) {
195 throw new MessagingException("Invalid character encodings");
196 }
197
198 }
199
200 /**
201 * Evaluate a DIGEST-MD5 login client authentication challenge, returning
202 * the a result string that should satisfy the clallenge.
203 *
204 * @param challenge
205 * The decoded challenge data, as a string.
206 *
207 * @return A formatted challege response, as an array of bytes.
208 * @exception MessagingException
209 */
210 public byte[] authenticateClient(byte[] challenge) throws MessagingException {
211
212 if (!parseChallenge(challenge)) {
213 return null;
214 }
215
216 SecureRandom randomGenerator;
217
218
219 try {
220 randomGenerator = new SecureRandom();
221 digest = MessageDigest.getInstance("MD5");
222 } catch (NoSuchAlgorithmException e) {
223 throw new MessagingException("Unable to access cryptography libraries");
224 }
225
226
227
228 if (realm == null) {
229
230 if (realms.isEmpty()) {
231 realm = host;
232 } else {
233
234 realm = (String) realms.get(0);
235 }
236 }
237
238
239
240 byte[] cnonceBytes = new byte[32];
241
242 randomGenerator.nextBytes(cnonceBytes);
243
244 String cnonce = new String(Base64.encode(cnonceBytes));
245
246
247
248
249 try {
250
251 String idString = username + ":" + realm + ":" + password;
252
253
254
255 digest.update(digest.digest(idString.getBytes("US-ASCII")));
256
257
258 String nonceString = ":" + nonce + ":" + cnonce;
259 digest.update(nonceString.getBytes("US-ASCII"));
260
261
262
263
264
265
266 clientResponse = new String(Hex.encode(digest.digest())) + ":" + nonce + ":00000001:" + cnonce + ":auth:";
267
268
269 String authString = "AUTHENTICATE:smtp/" + host;
270 digest.update(authString.getBytes("US-ASCII"));
271
272
273 String responseString = clientResponse + new String(Hex.encode(digest.digest()));
274
275 digest.update(responseString.getBytes("US-ASCII"));
276
277
278
279 String challengeResponse = new String(Hex.encode(digest.digest()));
280
281
282
283
284 StringBuffer response = new StringBuffer();
285
286 response.append("username=\"");
287 response.append(username);
288 response.append("\"");
289
290 response.append(",realm=\"");
291 response.append(realm);
292 response.append("\"");
293
294
295
296 response.append(",qop=auth");
297 response.append(",nc=00000001");
298
299 response.append(",nonce=\"");
300 response.append(nonce);
301 response.append("\"");
302
303 response.append(",cnonce=\"");
304 response.append(cnonce);
305 response.append("\"");
306
307 response.append(",digest-uri=\"smtp/");
308 response.append(host);
309 response.append("\"");
310
311 response.append(",response=");
312 response.append(challengeResponse);
313
314 return response.toString().getBytes("US-ASCII");
315
316 } catch (UnsupportedEncodingException e) {
317 throw new MessagingException("Invalid character encodings");
318 }
319 }
320
321 /**
322 * Parse the challege string, pulling out information required for our
323 * challenge response.
324 *
325 * @param challenge
326 * The challenge data.
327 *
328 * @return true if there were no errors parsing the string, false otherwise.
329 * @exception MessagingException
330 */
331 protected boolean parseChallenge(byte[] challenge) throws MessagingException {
332 realms = new ArrayList();
333
334 DigestParser parser = new DigestParser(new String(challenge));
335
336
337
338 while (parser.hasMore()) {
339 NameValuePair pair = parser.parseNameValuePair();
340
341 String name = pair.name;
342
343
344 if (name.equalsIgnoreCase("realm")) {
345 realms.add(pair.value);
346 }
347
348 else if (name.equalsIgnoreCase("nonce")) {
349 nonce = pair.value;
350 }
351
352
353 else if (name.equalsIgnoreCase("rspauth")) {
354 authenticationResponse = pair.value;
355 }
356 }
357
358 return true;
359 }
360
361 /**
362 * Inner class for parsing a DIGEST-MD5 challenge string, which is composed
363 * of "name=value" pairs, separated by "," characters.
364 */
365 class DigestParser {
366
367 String challenge;
368
369
370 int length;
371
372
373 int position;
374
375 /**
376 * Normal constructor.
377 *
378 * @param challenge
379 * The challenge string to be parsed.
380 */
381 public DigestParser(String challenge) {
382 this.challenge = challenge;
383 this.length = challenge.length();
384 position = 0;
385 }
386
387 /**
388 * Test if there are more values to parse.
389 *
390 * @return true if we've not reached the end of the challenge string,
391 * false if the challenge has been completely consumed.
392 */
393 private boolean hasMore() {
394 return position < length;
395 }
396
397 /**
398 * Return the character at the current parsing position.
399 *
400 * @return The string character for the current parse position.
401 */
402 private char currentChar() {
403 return challenge.charAt(position);
404 }
405
406 /**
407 * step forward to the next character position.
408 */
409 private void nextChar() {
410 position++;
411 }
412
413 /**
414 * Skip over any white space characters in the challenge string.
415 */
416 private void skipSpaces() {
417 while (position < length && Character.isWhitespace(currentChar())) {
418 position++;
419 }
420 }
421
422 /**
423 * Parse a quoted string used with a name/value pair, accounting for
424 * escape characters embedded within the string.
425 *
426 * @return The string value of the character string.
427 */
428 private String parseQuotedValue() {
429
430
431
432 nextChar();
433
434 StringBuffer value = new StringBuffer();
435
436 while (hasMore()) {
437 char ch = currentChar();
438
439
440 if (ch == '\\') {
441
442 nextChar();
443
444 if (!hasMore()) {
445 return null;
446 }
447 value.append(currentChar());
448 }
449
450 else if (ch == '"') {
451
452 nextChar();
453
454 return value.toString();
455 } else {
456
457
458 value.append(ch);
459 }
460 nextChar();
461 }
462
463 return null;
464 }
465
466 /**
467 * Parse a token value used with a name/value pair.
468 *
469 * @return The string value of the token. Returns null if nothing is
470 * found up to the separater.
471 */
472 private String parseTokenValue() {
473
474 StringBuffer value = new StringBuffer();
475
476 while (hasMore()) {
477 char ch = currentChar();
478 switch (ch) {
479
480 case ' ':
481 case '\t':
482 case '(':
483 case ')':
484 case '<':
485 case '>':
486 case '@':
487 case ',':
488 case ';':
489 case ':':
490 case '\\':
491 case '"':
492 case '/':
493 case '[':
494 case ']':
495 case '?':
496 case '=':
497 case '{':
498 case '}':
499
500 if (value.length() == 0) {
501 return null;
502 }
503
504 return value.toString();
505
506 default:
507
508
509
510 if (ch < 32 || ch > 127) {
511
512 if (value.length() == 0) {
513 return null;
514 }
515
516 return value.toString();
517 }
518 value.append(ch);
519 break;
520 }
521
522 nextChar();
523 }
524
525 if (value.length() == 0) {
526 return null;
527 }
528
529 return value.toString();
530 }
531
532 /**
533 * Parse out a name token of a name/value pair.
534 *
535 * @return The string value of the name.
536 */
537 private String parseName() {
538
539 skipSpaces();
540
541
542 return parseTokenValue();
543 }
544
545 /**
546 * Parse out a a value of a name/value pair.
547 *
548 * @return The string value associated with the name.
549 */
550 private String parseValue() {
551
552 skipSpaces();
553
554
555 if (currentChar() == '"') {
556
557 return parseQuotedValue();
558 }
559
560 return parseTokenValue();
561 }
562
563 /**
564 * Parse a name/value pair in an DIGEST-MD5 string.
565 *
566 * @return A NameValuePair object containing the two parts of the value.
567 * @exception MessagingException
568 */
569 public NameValuePair parseNameValuePair() throws MessagingException {
570
571 String name = parseName();
572 if (name == null) {
573 throw new MessagingException("Name syntax error");
574 }
575
576
577 if (!hasMore() || currentChar() != '=') {
578 throw new MessagingException("Name/value pair syntax error");
579 }
580
581
582 nextChar();
583
584
585 String value = parseValue();
586 if (value == null) {
587 throw new MessagingException("Name/value pair syntax error");
588 }
589
590
591
592 skipSpaces();
593
594 if (hasMore()) {
595 if (currentChar() != ',') {
596 throw new MessagingException("Name/value pair syntax error");
597 }
598
599
600
601 nextChar();
602 skipSpaces();
603 }
604 return new NameValuePair(name, value);
605 }
606 }
607
608 /**
609 * Simple inner class to represent a name/value pair.
610 */
611 public class NameValuePair {
612 public String name;
613
614 public String value;
615
616 NameValuePair(String name, String value) {
617 this.name = name;
618 this.value = value;
619 }
620
621 }
622 }