mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial