001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027package org.apache.http.impl.auth; 028 029import java.io.IOException; 030import java.nio.charset.Charset; 031import java.security.MessageDigest; 032import java.security.SecureRandom; 033import java.util.ArrayList; 034import java.util.Formatter; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Locale; 038import java.util.Set; 039import java.util.StringTokenizer; 040 041import org.apache.http.Consts; 042import org.apache.http.Header; 043import org.apache.http.HttpEntity; 044import org.apache.http.HttpEntityEnclosingRequest; 045import org.apache.http.HttpRequest; 046import org.apache.http.auth.AUTH; 047import org.apache.http.auth.AuthenticationException; 048import org.apache.http.auth.ChallengeState; 049import org.apache.http.auth.Credentials; 050import org.apache.http.auth.MalformedChallengeException; 051import org.apache.http.message.BasicHeaderValueFormatter; 052import org.apache.http.message.BasicNameValuePair; 053import org.apache.http.message.BufferedHeader; 054import org.apache.http.protocol.BasicHttpContext; 055import org.apache.http.protocol.HttpContext; 056import org.apache.http.util.Args; 057import org.apache.http.util.CharArrayBuffer; 058import org.apache.http.util.EncodingUtils; 059 060/** 061 * Digest authentication scheme as defined in RFC 2617. 062 * Both MD5 (default) and MD5-sess are supported. 063 * Currently only qop=auth or no qop is supported. qop=auth-int 064 * is unsupported. If auth and auth-int are provided, auth is 065 * used. 066 * <p> 067 * Since the digest username is included as clear text in the generated 068 * Authentication header, the charset of the username must be compatible 069 * with the HTTP element charset used by the connection. 070 * </p> 071 * 072 * @since 4.0 073 */ 074public class DigestScheme extends RFC2617Scheme { 075 076 private static final long serialVersionUID = 3883908186234566916L; 077 078 /** 079 * Hexa values used when creating 32 character long digest in HTTP DigestScheme 080 * in case of authentication. 081 * 082 * @see #encode(byte[]) 083 */ 084 private static final char[] HEXADECIMAL = { 085 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 086 'e', 'f' 087 }; 088 089 /** Whether the digest authentication process is complete */ 090 private boolean complete; 091 092 private static final int QOP_UNKNOWN = -1; 093 private static final int QOP_MISSING = 0; 094 private static final int QOP_AUTH_INT = 1; 095 private static final int QOP_AUTH = 2; 096 097 private String lastNonce; 098 private long nounceCount; 099 private String cnonce; 100 private String a1; 101 private String a2; 102 103 /** 104 * @since 4.3 105 */ 106 public DigestScheme(final Charset credentialsCharset) { 107 super(credentialsCharset); 108 this.complete = false; 109 } 110 111 /** 112 * Creates an instance of {@code DigestScheme} with the given challenge 113 * state. 114 * 115 * @since 4.2 116 * 117 * @deprecated (4.3) do not use. 118 */ 119 @Deprecated 120 public DigestScheme(final ChallengeState challengeState) { 121 super(challengeState); 122 } 123 124 public DigestScheme() { 125 this(Consts.ASCII); 126 } 127 128 /** 129 * Processes the Digest challenge. 130 * 131 * @param header the challenge header 132 * 133 * @throws MalformedChallengeException is thrown if the authentication challenge 134 * is malformed 135 */ 136 @Override 137 public void processChallenge( 138 final Header header) throws MalformedChallengeException { 139 super.processChallenge(header); 140 this.complete = true; 141 if (getParameters().isEmpty()) { 142 throw new MalformedChallengeException("Authentication challenge is empty"); 143 } 144 } 145 146 /** 147 * Tests if the Digest authentication process has been completed. 148 * 149 * @return {@code true} if Digest authorization has been processed, 150 * {@code false} otherwise. 151 */ 152 @Override 153 public boolean isComplete() { 154 final String s = getParameter("stale"); 155 if ("true".equalsIgnoreCase(s)) { 156 return false; 157 } else { 158 return this.complete; 159 } 160 } 161 162 /** 163 * Returns textual designation of the digest authentication scheme. 164 * 165 * @return {@code digest} 166 */ 167 @Override 168 public String getSchemeName() { 169 return "digest"; 170 } 171 172 /** 173 * Returns {@code false}. Digest authentication scheme is request based. 174 * 175 * @return {@code false}. 176 */ 177 @Override 178 public boolean isConnectionBased() { 179 return false; 180 } 181 182 public void overrideParamter(final String name, final String value) { 183 getParameters().put(name, value); 184 } 185 186 /** 187 * @deprecated (4.2) Use {@link org.apache.http.auth.ContextAwareAuthScheme#authenticate( 188 * Credentials, HttpRequest, org.apache.http.protocol.HttpContext)} 189 */ 190 @Override 191 @Deprecated 192 public Header authenticate( 193 final Credentials credentials, final HttpRequest request) throws AuthenticationException { 194 return authenticate(credentials, request, new BasicHttpContext()); 195 } 196 197 /** 198 * Produces a digest authorization string for the given set of 199 * {@link Credentials}, method name and URI. 200 * 201 * @param credentials A set of credentials to be used for athentication 202 * @param request The request being authenticated 203 * 204 * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials 205 * are not valid or not applicable for this authentication scheme 206 * @throws AuthenticationException if authorization string cannot 207 * be generated due to an authentication failure 208 * 209 * @return a digest authorization string 210 */ 211 @Override 212 public Header authenticate( 213 final Credentials credentials, 214 final HttpRequest request, 215 final HttpContext context) throws AuthenticationException { 216 217 Args.notNull(credentials, "Credentials"); 218 Args.notNull(request, "HTTP request"); 219 if (getParameter("realm") == null) { 220 throw new AuthenticationException("missing realm in challenge"); 221 } 222 if (getParameter("nonce") == null) { 223 throw new AuthenticationException("missing nonce in challenge"); 224 } 225 // Add method name and request-URI to the parameter map 226 getParameters().put("methodname", request.getRequestLine().getMethod()); 227 getParameters().put("uri", request.getRequestLine().getUri()); 228 final String charset = getParameter("charset"); 229 if (charset == null) { 230 getParameters().put("charset", getCredentialsCharset(request)); 231 } 232 return createDigestHeader(credentials, request); 233 } 234 235 private static MessageDigest createMessageDigest( 236 final String digAlg) throws UnsupportedDigestAlgorithmException { 237 try { 238 return MessageDigest.getInstance(digAlg); 239 } catch (final Exception e) { 240 throw new UnsupportedDigestAlgorithmException( 241 "Unsupported algorithm in HTTP Digest authentication: " 242 + digAlg); 243 } 244 } 245 246 /** 247 * Creates digest-response header as defined in RFC2617. 248 * 249 * @param credentials User credentials 250 * 251 * @return The digest-response as String. 252 */ 253 private Header createDigestHeader( 254 final Credentials credentials, 255 final HttpRequest request) throws AuthenticationException { 256 final String uri = getParameter("uri"); 257 final String realm = getParameter("realm"); 258 final String nonce = getParameter("nonce"); 259 final String opaque = getParameter("opaque"); 260 final String method = getParameter("methodname"); 261 String algorithm = getParameter("algorithm"); 262 // If an algorithm is not specified, default to MD5. 263 if (algorithm == null) { 264 algorithm = "MD5"; 265 } 266 267 final Set<String> qopset = new HashSet<String>(8); 268 int qop = QOP_UNKNOWN; 269 final String qoplist = getParameter("qop"); 270 if (qoplist != null) { 271 final StringTokenizer tok = new StringTokenizer(qoplist, ","); 272 while (tok.hasMoreTokens()) { 273 final String variant = tok.nextToken().trim(); 274 qopset.add(variant.toLowerCase(Locale.ROOT)); 275 } 276 if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) { 277 qop = QOP_AUTH_INT; 278 } else if (qopset.contains("auth")) { 279 qop = QOP_AUTH; 280 } 281 } else { 282 qop = QOP_MISSING; 283 } 284 285 if (qop == QOP_UNKNOWN) { 286 throw new AuthenticationException("None of the qop methods is supported: " + qoplist); 287 } 288 289 String charset = getParameter("charset"); 290 if (charset == null) { 291 charset = "ISO-8859-1"; 292 } 293 294 String digAlg = algorithm; 295 if (digAlg.equalsIgnoreCase("MD5-sess")) { 296 digAlg = "MD5"; 297 } 298 299 final MessageDigest digester; 300 try { 301 digester = createMessageDigest(digAlg); 302 } catch (final UnsupportedDigestAlgorithmException ex) { 303 throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg); 304 } 305 306 final String uname = credentials.getUserPrincipal().getName(); 307 final String pwd = credentials.getPassword(); 308 309 if (nonce.equals(this.lastNonce)) { 310 nounceCount++; 311 } else { 312 nounceCount = 1; 313 cnonce = null; 314 lastNonce = nonce; 315 } 316 final StringBuilder sb = new StringBuilder(256); 317 final Formatter formatter = new Formatter(sb, Locale.US); 318 formatter.format("%08x", Long.valueOf(nounceCount)); 319 formatter.close(); 320 final String nc = sb.toString(); 321 322 if (cnonce == null) { 323 cnonce = createCnonce(); 324 } 325 326 a1 = null; 327 a2 = null; 328 // 3.2.2.2: Calculating digest 329 if (algorithm.equalsIgnoreCase("MD5-sess")) { 330 // H( unq(username-value) ":" unq(realm-value) ":" passwd ) 331 // ":" unq(nonce-value) 332 // ":" unq(cnonce-value) 333 334 // calculated one per session 335 sb.setLength(0); 336 sb.append(uname).append(':').append(realm).append(':').append(pwd); 337 final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset))); 338 sb.setLength(0); 339 sb.append(checksum).append(':').append(nonce).append(':').append(cnonce); 340 a1 = sb.toString(); 341 } else { 342 // unq(username-value) ":" unq(realm-value) ":" passwd 343 sb.setLength(0); 344 sb.append(uname).append(':').append(realm).append(':').append(pwd); 345 a1 = sb.toString(); 346 } 347 348 final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset))); 349 350 if (qop == QOP_AUTH) { 351 // Method ":" digest-uri-value 352 a2 = method + ':' + uri; 353 } else if (qop == QOP_AUTH_INT) { 354 // Method ":" digest-uri-value ":" H(entity-body) 355 HttpEntity entity = null; 356 if (request instanceof HttpEntityEnclosingRequest) { 357 entity = ((HttpEntityEnclosingRequest) request).getEntity(); 358 } 359 if (entity != null && !entity.isRepeatable()) { 360 // If the entity is not repeatable, try falling back onto QOP_AUTH 361 if (qopset.contains("auth")) { 362 qop = QOP_AUTH; 363 a2 = method + ':' + uri; 364 } else { 365 throw new AuthenticationException("Qop auth-int cannot be used with " + 366 "a non-repeatable entity"); 367 } 368 } else { 369 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester); 370 try { 371 if (entity != null) { 372 entity.writeTo(entityDigester); 373 } 374 entityDigester.close(); 375 } catch (final IOException ex) { 376 throw new AuthenticationException("I/O error reading entity content", ex); 377 } 378 a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest()); 379 } 380 } else { 381 a2 = method + ':' + uri; 382 } 383 384 final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset))); 385 386 // 3.2.2.1 387 388 final String digestValue; 389 if (qop == QOP_MISSING) { 390 sb.setLength(0); 391 sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2); 392 digestValue = sb.toString(); 393 } else { 394 sb.setLength(0); 395 sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':') 396 .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth") 397 .append(':').append(hasha2); 398 digestValue = sb.toString(); 399 } 400 401 final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue))); 402 403 final CharArrayBuffer buffer = new CharArrayBuffer(128); 404 if (isProxy()) { 405 buffer.append(AUTH.PROXY_AUTH_RESP); 406 } else { 407 buffer.append(AUTH.WWW_AUTH_RESP); 408 } 409 buffer.append(": Digest "); 410 411 final List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); 412 params.add(new BasicNameValuePair("username", uname)); 413 params.add(new BasicNameValuePair("realm", realm)); 414 params.add(new BasicNameValuePair("nonce", nonce)); 415 params.add(new BasicNameValuePair("uri", uri)); 416 params.add(new BasicNameValuePair("response", digest)); 417 418 if (qop != QOP_MISSING) { 419 params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth")); 420 params.add(new BasicNameValuePair("nc", nc)); 421 params.add(new BasicNameValuePair("cnonce", cnonce)); 422 } 423 // algorithm cannot be null here 424 params.add(new BasicNameValuePair("algorithm", algorithm)); 425 if (opaque != null) { 426 params.add(new BasicNameValuePair("opaque", opaque)); 427 } 428 429 for (int i = 0; i < params.size(); i++) { 430 final BasicNameValuePair param = params.get(i); 431 if (i > 0) { 432 buffer.append(", "); 433 } 434 final String name = param.getName(); 435 final boolean noQuotes = ("nc".equals(name) || "qop".equals(name) 436 || "algorithm".equals(name)); 437 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes); 438 } 439 return new BufferedHeader(buffer); 440 } 441 442 String getCnonce() { 443 return cnonce; 444 } 445 446 String getA1() { 447 return a1; 448 } 449 450 String getA2() { 451 return a2; 452 } 453 454 /** 455 * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long 456 * <CODE>String</CODE> according to RFC 2617. 457 * 458 * @param binaryData array containing the digest 459 * @return encoded MD5, or <CODE>null</CODE> if encoding failed 460 */ 461 static String encode(final byte[] binaryData) { 462 final int n = binaryData.length; 463 final char[] buffer = new char[n * 2]; 464 for (int i = 0; i < n; i++) { 465 final int low = (binaryData[i] & 0x0f); 466 final int high = ((binaryData[i] & 0xf0) >> 4); 467 buffer[i * 2] = HEXADECIMAL[high]; 468 buffer[(i * 2) + 1] = HEXADECIMAL[low]; 469 } 470 471 return new String(buffer); 472 } 473 474 475 /** 476 * Creates a random cnonce value based on the current time. 477 * 478 * @return The cnonce value as String. 479 */ 480 public static String createCnonce() { 481 final SecureRandom rnd = new SecureRandom(); 482 final byte[] tmp = new byte[8]; 483 rnd.nextBytes(tmp); 484 return encode(tmp); 485 } 486 487 @Override 488 public String toString() { 489 final StringBuilder builder = new StringBuilder(); 490 builder.append("DIGEST [complete=").append(complete) 491 .append(", nonce=").append(lastNonce) 492 .append(", nc=").append(nounceCount) 493 .append("]"); 494 return builder.toString(); 495 } 496 497}