1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,442 @@ 1.4 +/* 1.5 + * ==================================================================== 1.6 + * 1.7 + * Licensed to the Apache Software Foundation (ASF) under one or more 1.8 + * contributor license agreements. See the NOTICE file distributed with 1.9 + * this work for additional information regarding copyright ownership. 1.10 + * The ASF licenses this file to You under the Apache License, Version 2.0 1.11 + * (the "License"); you may not use this file except in compliance with 1.12 + * the License. You may obtain a copy of the License at 1.13 + * 1.14 + * http://www.apache.org/licenses/LICENSE-2.0 1.15 + * 1.16 + * Unless required by applicable law or agreed to in writing, software 1.17 + * distributed under the License is distributed on an "AS IS" BASIS, 1.18 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1.19 + * See the License for the specific language governing permissions and 1.20 + * limitations under the License. 1.21 + * ==================================================================== 1.22 + * 1.23 + * This software consists of voluntary contributions made by many 1.24 + * individuals on behalf of the Apache Software Foundation. For more 1.25 + * information on the Apache Software Foundation, please see 1.26 + * <http://www.apache.org/>. 1.27 + * 1.28 + */ 1.29 + 1.30 +package ch.boye.httpclientandroidlib.impl.auth; 1.31 + 1.32 +import java.security.MessageDigest; 1.33 +import java.security.SecureRandom; 1.34 +import java.util.ArrayList; 1.35 +import java.util.Formatter; 1.36 +import java.util.List; 1.37 +import java.util.Locale; 1.38 +import java.util.StringTokenizer; 1.39 + 1.40 +import ch.boye.httpclientandroidlib.annotation.NotThreadSafe; 1.41 + 1.42 +import ch.boye.httpclientandroidlib.Header; 1.43 +import ch.boye.httpclientandroidlib.HttpRequest; 1.44 +import ch.boye.httpclientandroidlib.auth.AuthenticationException; 1.45 +import ch.boye.httpclientandroidlib.auth.Credentials; 1.46 +import ch.boye.httpclientandroidlib.auth.AUTH; 1.47 +import ch.boye.httpclientandroidlib.auth.MalformedChallengeException; 1.48 +import ch.boye.httpclientandroidlib.auth.params.AuthParams; 1.49 +import ch.boye.httpclientandroidlib.message.BasicNameValuePair; 1.50 +import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter; 1.51 +import ch.boye.httpclientandroidlib.message.BufferedHeader; 1.52 +import ch.boye.httpclientandroidlib.util.CharArrayBuffer; 1.53 +import ch.boye.httpclientandroidlib.util.EncodingUtils; 1.54 + 1.55 +/** 1.56 + * Digest authentication scheme as defined in RFC 2617. 1.57 + * Both MD5 (default) and MD5-sess are supported. 1.58 + * Currently only qop=auth or no qop is supported. qop=auth-int 1.59 + * is unsupported. If auth and auth-int are provided, auth is 1.60 + * used. 1.61 + * <p> 1.62 + * Credential charset is configured via the 1.63 + * {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET} 1.64 + * parameter of the HTTP request. 1.65 + * <p> 1.66 + * Since the digest username is included as clear text in the generated 1.67 + * Authentication header, the charset of the username must be compatible 1.68 + * with the 1.69 + * {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET 1.70 + * http element charset}. 1.71 + * <p> 1.72 + * The following parameters can be used to customize the behavior of this 1.73 + * class: 1.74 + * <ul> 1.75 + * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li> 1.76 + * </ul> 1.77 + * 1.78 + * @since 4.0 1.79 + */ 1.80 +@NotThreadSafe 1.81 +public class DigestScheme extends RFC2617Scheme { 1.82 + 1.83 + /** 1.84 + * Hexa values used when creating 32 character long digest in HTTP DigestScheme 1.85 + * in case of authentication. 1.86 + * 1.87 + * @see #encode(byte[]) 1.88 + */ 1.89 + private static final char[] HEXADECIMAL = { 1.90 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 1.91 + 'e', 'f' 1.92 + }; 1.93 + 1.94 + /** Whether the digest authentication process is complete */ 1.95 + private boolean complete; 1.96 + 1.97 + private static final int QOP_UNKNOWN = -1; 1.98 + private static final int QOP_MISSING = 0; 1.99 + private static final int QOP_AUTH_INT = 1; 1.100 + private static final int QOP_AUTH = 2; 1.101 + 1.102 + private String lastNonce; 1.103 + private long nounceCount; 1.104 + private String cnonce; 1.105 + private String a1; 1.106 + private String a2; 1.107 + 1.108 + /** 1.109 + * Default constructor for the digest authetication scheme. 1.110 + */ 1.111 + public DigestScheme() { 1.112 + super(); 1.113 + this.complete = false; 1.114 + } 1.115 + 1.116 + /** 1.117 + * Processes the Digest challenge. 1.118 + * 1.119 + * @param header the challenge header 1.120 + * 1.121 + * @throws MalformedChallengeException is thrown if the authentication challenge 1.122 + * is malformed 1.123 + */ 1.124 + @Override 1.125 + public void processChallenge( 1.126 + final Header header) throws MalformedChallengeException { 1.127 + super.processChallenge(header); 1.128 + 1.129 + if (getParameter("realm") == null) { 1.130 + throw new MalformedChallengeException("missing realm in challenge"); 1.131 + } 1.132 + if (getParameter("nonce") == null) { 1.133 + throw new MalformedChallengeException("missing nonce in challenge"); 1.134 + } 1.135 + this.complete = true; 1.136 + } 1.137 + 1.138 + /** 1.139 + * Tests if the Digest authentication process has been completed. 1.140 + * 1.141 + * @return <tt>true</tt> if Digest authorization has been processed, 1.142 + * <tt>false</tt> otherwise. 1.143 + */ 1.144 + public boolean isComplete() { 1.145 + String s = getParameter("stale"); 1.146 + if ("true".equalsIgnoreCase(s)) { 1.147 + return false; 1.148 + } else { 1.149 + return this.complete; 1.150 + } 1.151 + } 1.152 + 1.153 + /** 1.154 + * Returns textual designation of the digest authentication scheme. 1.155 + * 1.156 + * @return <code>digest</code> 1.157 + */ 1.158 + public String getSchemeName() { 1.159 + return "digest"; 1.160 + } 1.161 + 1.162 + /** 1.163 + * Returns <tt>false</tt>. Digest authentication scheme is request based. 1.164 + * 1.165 + * @return <tt>false</tt>. 1.166 + */ 1.167 + public boolean isConnectionBased() { 1.168 + return false; 1.169 + } 1.170 + 1.171 + public void overrideParamter(final String name, final String value) { 1.172 + getParameters().put(name, value); 1.173 + } 1.174 + 1.175 + /** 1.176 + * Produces a digest authorization string for the given set of 1.177 + * {@link Credentials}, method name and URI. 1.178 + * 1.179 + * @param credentials A set of credentials to be used for athentication 1.180 + * @param request The request being authenticated 1.181 + * 1.182 + * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials 1.183 + * are not valid or not applicable for this authentication scheme 1.184 + * @throws AuthenticationException if authorization string cannot 1.185 + * be generated due to an authentication failure 1.186 + * 1.187 + * @return a digest authorization string 1.188 + */ 1.189 + public Header authenticate( 1.190 + final Credentials credentials, 1.191 + final HttpRequest request) throws AuthenticationException { 1.192 + 1.193 + if (credentials == null) { 1.194 + throw new IllegalArgumentException("Credentials may not be null"); 1.195 + } 1.196 + if (request == null) { 1.197 + throw new IllegalArgumentException("HTTP request may not be null"); 1.198 + } 1.199 + 1.200 + // Add method name and request-URI to the parameter map 1.201 + getParameters().put("methodname", request.getRequestLine().getMethod()); 1.202 + getParameters().put("uri", request.getRequestLine().getUri()); 1.203 + String charset = getParameter("charset"); 1.204 + if (charset == null) { 1.205 + charset = AuthParams.getCredentialCharset(request.getParams()); 1.206 + getParameters().put("charset", charset); 1.207 + } 1.208 + return createDigestHeader(credentials); 1.209 + } 1.210 + 1.211 + private static MessageDigest createMessageDigest( 1.212 + final String digAlg) throws UnsupportedDigestAlgorithmException { 1.213 + try { 1.214 + return MessageDigest.getInstance(digAlg); 1.215 + } catch (Exception e) { 1.216 + throw new UnsupportedDigestAlgorithmException( 1.217 + "Unsupported algorithm in HTTP Digest authentication: " 1.218 + + digAlg); 1.219 + } 1.220 + } 1.221 + 1.222 + /** 1.223 + * Creates digest-response header as defined in RFC2617. 1.224 + * 1.225 + * @param credentials User credentials 1.226 + * 1.227 + * @return The digest-response as String. 1.228 + */ 1.229 + private Header createDigestHeader( 1.230 + final Credentials credentials) throws AuthenticationException { 1.231 + String uri = getParameter("uri"); 1.232 + String realm = getParameter("realm"); 1.233 + String nonce = getParameter("nonce"); 1.234 + String opaque = getParameter("opaque"); 1.235 + String method = getParameter("methodname"); 1.236 + String algorithm = getParameter("algorithm"); 1.237 + if (uri == null) { 1.238 + throw new IllegalStateException("URI may not be null"); 1.239 + } 1.240 + if (realm == null) { 1.241 + throw new IllegalStateException("Realm may not be null"); 1.242 + } 1.243 + if (nonce == null) { 1.244 + throw new IllegalStateException("Nonce may not be null"); 1.245 + } 1.246 + 1.247 + //TODO: add support for QOP_INT 1.248 + int qop = QOP_UNKNOWN; 1.249 + String qoplist = getParameter("qop"); 1.250 + if (qoplist != null) { 1.251 + StringTokenizer tok = new StringTokenizer(qoplist, ","); 1.252 + while (tok.hasMoreTokens()) { 1.253 + String variant = tok.nextToken().trim(); 1.254 + if (variant.equals("auth")) { 1.255 + qop = QOP_AUTH; 1.256 + break; 1.257 + } 1.258 + } 1.259 + } else { 1.260 + qop = QOP_MISSING; 1.261 + } 1.262 + 1.263 + if (qop == QOP_UNKNOWN) { 1.264 + throw new AuthenticationException("None of the qop methods is supported: " + qoplist); 1.265 + } 1.266 + 1.267 + // If an algorithm is not specified, default to MD5. 1.268 + if (algorithm == null) { 1.269 + algorithm = "MD5"; 1.270 + } 1.271 + // If an charset is not specified, default to ISO-8859-1. 1.272 + String charset = getParameter("charset"); 1.273 + if (charset == null) { 1.274 + charset = "ISO-8859-1"; 1.275 + } 1.276 + 1.277 + String digAlg = algorithm; 1.278 + if (digAlg.equalsIgnoreCase("MD5-sess")) { 1.279 + digAlg = "MD5"; 1.280 + } 1.281 + 1.282 + MessageDigest digester; 1.283 + try { 1.284 + digester = createMessageDigest(digAlg); 1.285 + } catch (UnsupportedDigestAlgorithmException ex) { 1.286 + throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg); 1.287 + } 1.288 + 1.289 + String uname = credentials.getUserPrincipal().getName(); 1.290 + String pwd = credentials.getPassword(); 1.291 + 1.292 + if (nonce.equals(this.lastNonce)) { 1.293 + nounceCount++; 1.294 + } else { 1.295 + nounceCount = 1; 1.296 + cnonce = null; 1.297 + lastNonce = nonce; 1.298 + } 1.299 + StringBuilder sb = new StringBuilder(256); 1.300 + Formatter formatter = new Formatter(sb, Locale.US); 1.301 + formatter.format("%08x", nounceCount); 1.302 + String nc = sb.toString(); 1.303 + 1.304 + if (cnonce == null) { 1.305 + cnonce = createCnonce(); 1.306 + } 1.307 + 1.308 + a1 = null; 1.309 + a2 = null; 1.310 + // 3.2.2.2: Calculating digest 1.311 + if (algorithm.equalsIgnoreCase("MD5-sess")) { 1.312 + // H( unq(username-value) ":" unq(realm-value) ":" passwd ) 1.313 + // ":" unq(nonce-value) 1.314 + // ":" unq(cnonce-value) 1.315 + 1.316 + // calculated one per session 1.317 + sb.setLength(0); 1.318 + sb.append(uname).append(':').append(realm).append(':').append(pwd); 1.319 + String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset))); 1.320 + sb.setLength(0); 1.321 + sb.append(checksum).append(':').append(nonce).append(':').append(cnonce); 1.322 + a1 = sb.toString(); 1.323 + } else { 1.324 + // unq(username-value) ":" unq(realm-value) ":" passwd 1.325 + sb.setLength(0); 1.326 + sb.append(uname).append(':').append(realm).append(':').append(pwd); 1.327 + a1 = sb.toString(); 1.328 + } 1.329 + 1.330 + String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset))); 1.331 + 1.332 + if (qop == QOP_AUTH) { 1.333 + // Method ":" digest-uri-value 1.334 + a2 = method + ':' + uri; 1.335 + } else if (qop == QOP_AUTH_INT) { 1.336 + // Method ":" digest-uri-value ":" H(entity-body) 1.337 + //TODO: calculate entity hash if entity is repeatable 1.338 + throw new AuthenticationException("qop-int method is not suppported"); 1.339 + } else { 1.340 + a2 = method + ':' + uri; 1.341 + } 1.342 + 1.343 + String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset))); 1.344 + 1.345 + // 3.2.2.1 1.346 + 1.347 + String digestValue; 1.348 + if (qop == QOP_MISSING) { 1.349 + sb.setLength(0); 1.350 + sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2); 1.351 + digestValue = sb.toString(); 1.352 + } else { 1.353 + sb.setLength(0); 1.354 + sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':') 1.355 + .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth") 1.356 + .append(':').append(hasha2); 1.357 + digestValue = sb.toString(); 1.358 + } 1.359 + 1.360 + String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue))); 1.361 + 1.362 + CharArrayBuffer buffer = new CharArrayBuffer(128); 1.363 + if (isProxy()) { 1.364 + buffer.append(AUTH.PROXY_AUTH_RESP); 1.365 + } else { 1.366 + buffer.append(AUTH.WWW_AUTH_RESP); 1.367 + } 1.368 + buffer.append(": Digest "); 1.369 + 1.370 + List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); 1.371 + params.add(new BasicNameValuePair("username", uname)); 1.372 + params.add(new BasicNameValuePair("realm", realm)); 1.373 + params.add(new BasicNameValuePair("nonce", nonce)); 1.374 + params.add(new BasicNameValuePair("uri", uri)); 1.375 + params.add(new BasicNameValuePair("response", digest)); 1.376 + 1.377 + if (qop != QOP_MISSING) { 1.378 + params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth")); 1.379 + params.add(new BasicNameValuePair("nc", nc)); 1.380 + params.add(new BasicNameValuePair("cnonce", cnonce)); 1.381 + } 1.382 + if (algorithm != null) { 1.383 + params.add(new BasicNameValuePair("algorithm", algorithm)); 1.384 + } 1.385 + if (opaque != null) { 1.386 + params.add(new BasicNameValuePair("opaque", opaque)); 1.387 + } 1.388 + 1.389 + for (int i = 0; i < params.size(); i++) { 1.390 + BasicNameValuePair param = params.get(i); 1.391 + if (i > 0) { 1.392 + buffer.append(", "); 1.393 + } 1.394 + boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName()); 1.395 + BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes); 1.396 + } 1.397 + return new BufferedHeader(buffer); 1.398 + } 1.399 + 1.400 + String getCnonce() { 1.401 + return cnonce; 1.402 + } 1.403 + 1.404 + String getA1() { 1.405 + return a1; 1.406 + } 1.407 + 1.408 + String getA2() { 1.409 + return a2; 1.410 + } 1.411 + 1.412 + /** 1.413 + * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long 1.414 + * <CODE>String</CODE> according to RFC 2617. 1.415 + * 1.416 + * @param binaryData array containing the digest 1.417 + * @return encoded MD5, or <CODE>null</CODE> if encoding failed 1.418 + */ 1.419 + private static String encode(byte[] binaryData) { 1.420 + int n = binaryData.length; 1.421 + char[] buffer = new char[n * 2]; 1.422 + for (int i = 0; i < n; i++) { 1.423 + int low = (binaryData[i] & 0x0f); 1.424 + int high = ((binaryData[i] & 0xf0) >> 4); 1.425 + buffer[i * 2] = HEXADECIMAL[high]; 1.426 + buffer[(i * 2) + 1] = HEXADECIMAL[low]; 1.427 + } 1.428 + 1.429 + return new String(buffer); 1.430 + } 1.431 + 1.432 + 1.433 + /** 1.434 + * Creates a random cnonce value based on the current time. 1.435 + * 1.436 + * @return The cnonce value as String. 1.437 + */ 1.438 + public static String createCnonce() { 1.439 + SecureRandom rnd = new SecureRandom(); 1.440 + byte[] tmp = new byte[8]; 1.441 + rnd.nextBytes(tmp); 1.442 + return encode(tmp); 1.443 + } 1.444 + 1.445 +}