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