1 /* 2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java $ 3 * $Revision: 659595 $ 4 * $Date: 2008-05-23 09:47:14 -0700 (Fri, 23 May 2008) $ 5 * 6 * ==================================================================== 7 * 8 * Licensed to the Apache Software Foundation (ASF) under one or more 9 * contributor license agreements. See the NOTICE file distributed with 10 * this work for additional information regarding copyright ownership. 11 * The ASF licenses this file to You under the Apache License, Version 2.0 12 * (the "License"); you may not use this file except in compliance with 13 * the License. You may obtain a copy of the License at 14 * 15 * http://www.apache.org/licenses/LICENSE-2.0 16 * 17 * Unless required by applicable law or agreed to in writing, software 18 * distributed under the License is distributed on an "AS IS" BASIS, 19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 * See the License for the specific language governing permissions and 21 * limitations under the License. 22 * ==================================================================== 23 * 24 * This software consists of voluntary contributions made by many 25 * individuals on behalf of the Apache Software Foundation. For more 26 * information on the Apache Software Foundation, please see 27 * <http://www.apache.org/>. 28 * 29 */ 30 31 package org.apache.http.impl.auth; 32 33 import java.security.MessageDigest; 34 import java.util.ArrayList; 35 import java.util.List; 36 import java.util.StringTokenizer; 37 38 import org.apache.http.Header; 39 import org.apache.http.HttpRequest; 40 import org.apache.http.auth.AuthenticationException; 41 import org.apache.http.auth.Credentials; 42 import org.apache.http.auth.AUTH; 43 import org.apache.http.auth.MalformedChallengeException; 44 import org.apache.http.auth.params.AuthParams; 45 import org.apache.http.message.BasicNameValuePair; 46 import org.apache.http.message.BasicHeaderValueFormatter; 47 import org.apache.http.message.BufferedHeader; 48 import org.apache.http.util.CharArrayBuffer; 49 import org.apache.http.util.EncodingUtils; 50 51 /** 52 * <p> 53 * Digest authentication scheme as defined in RFC 2617. 54 * Both MD5 (default) and MD5-sess are supported. 55 * Currently only qop=auth or no qop is supported. qop=auth-int 56 * is unsupported. If auth and auth-int are provided, auth is 57 * used. 58 * </p> 59 * <p> 60 * Credential charset is configured via the 61 * {@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET 62 * credential charset} parameter. 63 * Since the digest username is included as clear text in the generated 64 * Authentication header, the charset of the username must be compatible 65 * with the 66 * {@link org.apache.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET 67 * http element charset}. 68 * </p> 69 * 70 * @author <a href="mailto:remm (at) apache.org">Remy Maucherat</a> 71 * @author Rodney Waldhoff 72 * @author <a href="mailto:jsdever (at) apache.org">Jeff Dever</a> 73 * @author Ortwin Glueck 74 * @author Sean C. Sullivan 75 * @author <a href="mailto:adrian (at) ephox.com">Adrian Sutton</a> 76 * @author <a href="mailto:mbowler (at) GargoyleSoftware.com">Mike Bowler</a> 77 * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> 78 * 79 * @since 4.0 80 * 81 * @deprecated Please use {@link java.net.URL#openConnection} instead. 82 * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> 83 * for further details. 84 */ 85 86 @Deprecated 87 public class DigestScheme extends RFC2617Scheme { 88 89 /** 90 * Hexa values used when creating 32 character long digest in HTTP DigestScheme 91 * in case of authentication. 92 * 93 * @see #encode(byte[]) 94 */ 95 private static final char[] HEXADECIMAL = { 96 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 97 'e', 'f' 98 }; 99 100 /** Whether the digest authentication process is complete */ 101 private boolean complete; 102 103 //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay 104 private static final String NC = "00000001"; //nonce-count is always 1 105 private static final int QOP_MISSING = 0; 106 private static final int QOP_AUTH_INT = 1; 107 private static final int QOP_AUTH = 2; 108 109 private int qopVariant = QOP_MISSING; 110 private String cnonce; 111 112 /** 113 * Default constructor for the digest authetication scheme. 114 */ 115 public DigestScheme() { 116 super(); 117 this.complete = false; 118 } 119 120 /** 121 * Processes the Digest challenge. 122 * 123 * @param header the challenge header 124 * 125 * @throws MalformedChallengeException is thrown if the authentication challenge 126 * is malformed 127 */ 128 @Override 129 public void processChallenge( 130 final Header header) throws MalformedChallengeException { 131 super.processChallenge(header); 132 133 if (getParameter("realm") == null) { 134 throw new MalformedChallengeException("missing realm in challange"); 135 } 136 if (getParameter("nonce") == null) { 137 throw new MalformedChallengeException("missing nonce in challange"); 138 } 139 140 boolean unsupportedQop = false; 141 // qop parsing 142 String qop = getParameter("qop"); 143 if (qop != null) { 144 StringTokenizer tok = new StringTokenizer(qop,","); 145 while (tok.hasMoreTokens()) { 146 String variant = tok.nextToken().trim(); 147 if (variant.equals("auth")) { 148 qopVariant = QOP_AUTH; 149 break; //that's our favourite, because auth-int is unsupported 150 } else if (variant.equals("auth-int")) { 151 qopVariant = QOP_AUTH_INT; 152 } else { 153 unsupportedQop = true; 154 } 155 } 156 } 157 158 if (unsupportedQop && (qopVariant == QOP_MISSING)) { 159 throw new MalformedChallengeException("None of the qop methods is supported"); 160 } 161 // Reset cnonce 162 this.cnonce = null; 163 this.complete = true; 164 } 165 166 /** 167 * Tests if the Digest authentication process has been completed. 168 * 169 * @return <tt>true</tt> if Digest authorization has been processed, 170 * <tt>false</tt> otherwise. 171 */ 172 public boolean isComplete() { 173 String s = getParameter("stale"); 174 if ("true".equalsIgnoreCase(s)) { 175 return false; 176 } else { 177 return this.complete; 178 } 179 } 180 181 /** 182 * Returns textual designation of the digest authentication scheme. 183 * 184 * @return <code>digest</code> 185 */ 186 public String getSchemeName() { 187 return "digest"; 188 } 189 190 /** 191 * Returns <tt>false</tt>. Digest authentication scheme is request based. 192 * 193 * @return <tt>false</tt>. 194 */ 195 public boolean isConnectionBased() { 196 return false; 197 } 198 199 public void overrideParamter(final String name, final String value) { 200 getParameters().put(name, value); 201 } 202 203 private String getCnonce() { 204 if (this.cnonce == null) { 205 this.cnonce = createCnonce(); 206 } 207 return this.cnonce; 208 } 209 210 /** 211 * Produces a digest authorization string for the given set of 212 * {@link Credentials}, method name and URI. 213 * 214 * @param credentials A set of credentials to be used for athentication 215 * @param request The request being authenticated 216 * 217 * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials 218 * are not valid or not applicable for this authentication scheme 219 * @throws AuthenticationException if authorization string cannot 220 * be generated due to an authentication failure 221 * 222 * @return a digest authorization string 223 */ 224 public Header authenticate( 225 final Credentials credentials, 226 final HttpRequest request) throws AuthenticationException { 227 228 if (credentials == null) { 229 throw new IllegalArgumentException("Credentials may not be null"); 230 } 231 if (request == null) { 232 throw new IllegalArgumentException("HTTP request may not be null"); 233 } 234 235 // Add method name and request-URI to the parameter map 236 getParameters().put("methodname", request.getRequestLine().getMethod()); 237 getParameters().put("uri", request.getRequestLine().getUri()); 238 String charset = getParameter("charset"); 239 if (charset == null) { 240 charset = AuthParams.getCredentialCharset(request.getParams()); 241 getParameters().put("charset", charset); 242 } 243 String digest = createDigest(credentials); 244 return createDigestHeader(credentials, digest); 245 } 246 247 private static MessageDigest createMessageDigest( 248 final String digAlg) throws UnsupportedDigestAlgorithmException { 249 try { 250 return MessageDigest.getInstance(digAlg); 251 } catch (Exception e) { 252 throw new UnsupportedDigestAlgorithmException( 253 "Unsupported algorithm in HTTP Digest authentication: " 254 + digAlg); 255 } 256 } 257 258 /** 259 * Creates an MD5 response digest. 260 * 261 * @return The created digest as string. This will be the response tag's 262 * value in the Authentication HTTP header. 263 * @throws AuthenticationException when MD5 is an unsupported algorithm 264 */ 265 private String createDigest(final Credentials credentials) throws AuthenticationException { 266 // Collecting required tokens 267 String uri = getParameter("uri"); 268 String realm = getParameter("realm"); 269 String nonce = getParameter("nonce"); 270 String method = getParameter("methodname"); 271 String algorithm = getParameter("algorithm"); 272 if (uri == null) { 273 throw new IllegalStateException("URI may not be null"); 274 } 275 if (realm == null) { 276 throw new IllegalStateException("Realm may not be null"); 277 } 278 if (nonce == null) { 279 throw new IllegalStateException("Nonce may not be null"); 280 } 281 // If an algorithm is not specified, default to MD5. 282 if (algorithm == null) { 283 algorithm = "MD5"; 284 } 285 // If an charset is not specified, default to ISO-8859-1. 286 String charset = getParameter("charset"); 287 if (charset == null) { 288 charset = "ISO-8859-1"; 289 } 290 291 if (qopVariant == QOP_AUTH_INT) { 292 throw new AuthenticationException( 293 "Unsupported qop in HTTP Digest authentication"); 294 } 295 296 MessageDigest md5Helper = createMessageDigest("MD5"); 297 298 String uname = credentials.getUserPrincipal().getName(); 299 String pwd = credentials.getPassword(); 300 301 // 3.2.2.2: Calculating digest 302 StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2); 303 tmp.append(uname); 304 tmp.append(':'); 305 tmp.append(realm); 306 tmp.append(':'); 307 tmp.append(pwd); 308 // unq(username-value) ":" unq(realm-value) ":" passwd 309 String a1 = tmp.toString(); 310 311 //a1 is suitable for MD5 algorithm 312 if(algorithm.equalsIgnoreCase("MD5-sess")) { // android-changed: ignore case 313 // H( unq(username-value) ":" unq(realm-value) ":" passwd ) 314 // ":" unq(nonce-value) 315 // ":" unq(cnonce-value) 316 317 String cnonce = getCnonce(); 318 319 String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); 320 StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2); 321 tmp3.append(tmp2); 322 tmp3.append(':'); 323 tmp3.append(nonce); 324 tmp3.append(':'); 325 tmp3.append(cnonce); 326 a1 = tmp3.toString(); 327 } else if (!algorithm.equalsIgnoreCase("MD5")) { // android-changed: ignore case 328 throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested"); 329 } 330 String md5a1 = encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); 331 332 String a2 = null; 333 if (qopVariant == QOP_AUTH_INT) { 334 // Unhandled qop auth-int 335 //we do not have access to the entity-body or its hash 336 //TODO: add Method ":" digest-uri-value ":" H(entity-body) 337 } else { 338 a2 = method + ':' + uri; 339 } 340 String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2))); 341 342 // 3.2.2.1 343 String serverDigestValue; 344 if (qopVariant == QOP_MISSING) { 345 StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + md5a2.length()); 346 tmp2.append(md5a1); 347 tmp2.append(':'); 348 tmp2.append(nonce); 349 tmp2.append(':'); 350 tmp2.append(md5a2); 351 serverDigestValue = tmp2.toString(); 352 } else { 353 String qopOption = getQopVariantString(); 354 String cnonce = getCnonce(); 355 356 StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() 357 + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5); 358 tmp2.append(md5a1); 359 tmp2.append(':'); 360 tmp2.append(nonce); 361 tmp2.append(':'); 362 tmp2.append(NC); 363 tmp2.append(':'); 364 tmp2.append(cnonce); 365 tmp2.append(':'); 366 tmp2.append(qopOption); 367 tmp2.append(':'); 368 tmp2.append(md5a2); 369 serverDigestValue = tmp2.toString(); 370 } 371 372 String serverDigest = 373 encode(md5Helper.digest(EncodingUtils.getAsciiBytes(serverDigestValue))); 374 375 return serverDigest; 376 } 377 378 /** 379 * Creates digest-response header as defined in RFC2617. 380 * 381 * @param credentials User credentials 382 * @param digest The response tag's value as String. 383 * 384 * @return The digest-response as String. 385 */ 386 private Header createDigestHeader( 387 final Credentials credentials, 388 final String digest) throws AuthenticationException { 389 390 CharArrayBuffer buffer = new CharArrayBuffer(128); 391 if (isProxy()) { 392 buffer.append(AUTH.PROXY_AUTH_RESP); 393 } else { 394 buffer.append(AUTH.WWW_AUTH_RESP); 395 } 396 buffer.append(": Digest "); 397 398 String uri = getParameter("uri"); 399 String realm = getParameter("realm"); 400 String nonce = getParameter("nonce"); 401 String opaque = getParameter("opaque"); 402 String response = digest; 403 String algorithm = getParameter("algorithm"); 404 405 String uname = credentials.getUserPrincipal().getName(); 406 407 List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); 408 params.add(new BasicNameValuePair("username", uname)); 409 params.add(new BasicNameValuePair("realm", realm)); 410 params.add(new BasicNameValuePair("nonce", nonce)); 411 params.add(new BasicNameValuePair("uri", uri)); 412 params.add(new BasicNameValuePair("response", response)); 413 414 if (qopVariant != QOP_MISSING) { 415 params.add(new BasicNameValuePair("qop", getQopVariantString())); 416 params.add(new BasicNameValuePair("nc", NC)); 417 params.add(new BasicNameValuePair("cnonce", getCnonce())); 418 } 419 if (algorithm != null) { 420 params.add(new BasicNameValuePair("algorithm", algorithm)); 421 } 422 if (opaque != null) { 423 params.add(new BasicNameValuePair("opaque", opaque)); 424 } 425 426 for (int i = 0; i < params.size(); i++) { 427 BasicNameValuePair param = params.get(i); 428 if (i > 0) { 429 buffer.append(", "); 430 } 431 boolean noQuotes = "nc".equals(param.getName()) || 432 "qop".equals(param.getName()); 433 BasicHeaderValueFormatter.DEFAULT 434 .formatNameValuePair(buffer, param, !noQuotes); 435 } 436 return new BufferedHeader(buffer); 437 } 438 439 private String getQopVariantString() { 440 String qopOption; 441 if (qopVariant == QOP_AUTH_INT) { 442 qopOption = "auth-int"; 443 } else { 444 qopOption = "auth"; 445 } 446 return qopOption; 447 } 448 449 /** 450 * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long 451 * <CODE>String</CODE> according to RFC 2617. 452 * 453 * @param binaryData array containing the digest 454 * @return encoded MD5, or <CODE>null</CODE> if encoding failed 455 */ 456 private static String encode(byte[] binaryData) { 457 if (binaryData.length != 16) { 458 return null; 459 } 460 461 char[] buffer = new char[32]; 462 for (int i = 0; i < 16; i++) { 463 int low = (binaryData[i] & 0x0f); 464 int high = ((binaryData[i] & 0xf0) >> 4); 465 buffer[i * 2] = HEXADECIMAL[high]; 466 buffer[(i * 2) + 1] = HEXADECIMAL[low]; 467 } 468 469 return new String(buffer); 470 } 471 472 473 /** 474 * Creates a random cnonce value based on the current time. 475 * 476 * @return The cnonce value as String. 477 * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported. 478 */ 479 public static String createCnonce() { 480 String cnonce; 481 482 MessageDigest md5Helper = createMessageDigest("MD5"); 483 484 cnonce = Long.toString(System.currentTimeMillis()); 485 cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce))); 486 487 return cnonce; 488 } 489 } 490