michael@0: /* michael@0: * ==================================================================== michael@0: * michael@0: * Licensed to the Apache Software Foundation (ASF) under one or more michael@0: * contributor license agreements. See the NOTICE file distributed with michael@0: * this work for additional information regarding copyright ownership. michael@0: * The ASF licenses this file to You under the Apache License, Version 2.0 michael@0: * (the "License"); you may not use this file except in compliance with michael@0: * the License. You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: * ==================================================================== michael@0: * michael@0: * This software consists of voluntary contributions made by many michael@0: * individuals on behalf of the Apache Software Foundation. For more michael@0: * information on the Apache Software Foundation, please see michael@0: * . michael@0: * michael@0: */ michael@0: michael@0: package ch.boye.httpclientandroidlib.impl.auth; michael@0: michael@0: import java.security.MessageDigest; michael@0: import java.security.SecureRandom; michael@0: import java.util.ArrayList; michael@0: import java.util.Formatter; michael@0: import java.util.List; michael@0: import java.util.Locale; michael@0: import java.util.StringTokenizer; michael@0: michael@0: import ch.boye.httpclientandroidlib.annotation.NotThreadSafe; michael@0: michael@0: import ch.boye.httpclientandroidlib.Header; michael@0: import ch.boye.httpclientandroidlib.HttpRequest; michael@0: import ch.boye.httpclientandroidlib.auth.AuthenticationException; michael@0: import ch.boye.httpclientandroidlib.auth.Credentials; michael@0: import ch.boye.httpclientandroidlib.auth.AUTH; michael@0: import ch.boye.httpclientandroidlib.auth.MalformedChallengeException; michael@0: import ch.boye.httpclientandroidlib.auth.params.AuthParams; michael@0: import ch.boye.httpclientandroidlib.message.BasicNameValuePair; michael@0: import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter; michael@0: import ch.boye.httpclientandroidlib.message.BufferedHeader; michael@0: import ch.boye.httpclientandroidlib.util.CharArrayBuffer; michael@0: import ch.boye.httpclientandroidlib.util.EncodingUtils; michael@0: michael@0: /** michael@0: * Digest authentication scheme as defined in RFC 2617. michael@0: * Both MD5 (default) and MD5-sess are supported. michael@0: * Currently only qop=auth or no qop is supported. qop=auth-int michael@0: * is unsupported. If auth and auth-int are provided, auth is michael@0: * used. michael@0: *

michael@0: * Credential charset is configured via the michael@0: * {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET} michael@0: * parameter of the HTTP request. michael@0: *

michael@0: * Since the digest username is included as clear text in the generated michael@0: * Authentication header, the charset of the username must be compatible michael@0: * with the michael@0: * {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET michael@0: * http element charset}. michael@0: *

michael@0: * The following parameters can be used to customize the behavior of this michael@0: * class: michael@0: *

michael@0: * michael@0: * @since 4.0 michael@0: */ michael@0: @NotThreadSafe michael@0: public class DigestScheme extends RFC2617Scheme { michael@0: michael@0: /** michael@0: * Hexa values used when creating 32 character long digest in HTTP DigestScheme michael@0: * in case of authentication. michael@0: * michael@0: * @see #encode(byte[]) michael@0: */ michael@0: private static final char[] HEXADECIMAL = { michael@0: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', michael@0: 'e', 'f' michael@0: }; michael@0: michael@0: /** Whether the digest authentication process is complete */ michael@0: private boolean complete; michael@0: michael@0: private static final int QOP_UNKNOWN = -1; michael@0: private static final int QOP_MISSING = 0; michael@0: private static final int QOP_AUTH_INT = 1; michael@0: private static final int QOP_AUTH = 2; michael@0: michael@0: private String lastNonce; michael@0: private long nounceCount; michael@0: private String cnonce; michael@0: private String a1; michael@0: private String a2; michael@0: michael@0: /** michael@0: * Default constructor for the digest authetication scheme. michael@0: */ michael@0: public DigestScheme() { michael@0: super(); michael@0: this.complete = false; michael@0: } michael@0: michael@0: /** michael@0: * Processes the Digest challenge. michael@0: * michael@0: * @param header the challenge header michael@0: * michael@0: * @throws MalformedChallengeException is thrown if the authentication challenge michael@0: * is malformed michael@0: */ michael@0: @Override michael@0: public void processChallenge( michael@0: final Header header) throws MalformedChallengeException { michael@0: super.processChallenge(header); michael@0: michael@0: if (getParameter("realm") == null) { michael@0: throw new MalformedChallengeException("missing realm in challenge"); michael@0: } michael@0: if (getParameter("nonce") == null) { michael@0: throw new MalformedChallengeException("missing nonce in challenge"); michael@0: } michael@0: this.complete = true; michael@0: } michael@0: michael@0: /** michael@0: * Tests if the Digest authentication process has been completed. michael@0: * michael@0: * @return true if Digest authorization has been processed, michael@0: * false otherwise. michael@0: */ michael@0: public boolean isComplete() { michael@0: String s = getParameter("stale"); michael@0: if ("true".equalsIgnoreCase(s)) { michael@0: return false; michael@0: } else { michael@0: return this.complete; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns textual designation of the digest authentication scheme. michael@0: * michael@0: * @return digest michael@0: */ michael@0: public String getSchemeName() { michael@0: return "digest"; michael@0: } michael@0: michael@0: /** michael@0: * Returns false. Digest authentication scheme is request based. michael@0: * michael@0: * @return false. michael@0: */ michael@0: public boolean isConnectionBased() { michael@0: return false; michael@0: } michael@0: michael@0: public void overrideParamter(final String name, final String value) { michael@0: getParameters().put(name, value); michael@0: } michael@0: michael@0: /** michael@0: * Produces a digest authorization string for the given set of michael@0: * {@link Credentials}, method name and URI. michael@0: * michael@0: * @param credentials A set of credentials to be used for athentication michael@0: * @param request The request being authenticated michael@0: * michael@0: * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials michael@0: * are not valid or not applicable for this authentication scheme michael@0: * @throws AuthenticationException if authorization string cannot michael@0: * be generated due to an authentication failure michael@0: * michael@0: * @return a digest authorization string michael@0: */ michael@0: public Header authenticate( michael@0: final Credentials credentials, michael@0: final HttpRequest request) throws AuthenticationException { michael@0: michael@0: if (credentials == null) { michael@0: throw new IllegalArgumentException("Credentials may not be null"); michael@0: } michael@0: if (request == null) { michael@0: throw new IllegalArgumentException("HTTP request may not be null"); michael@0: } michael@0: michael@0: // Add method name and request-URI to the parameter map michael@0: getParameters().put("methodname", request.getRequestLine().getMethod()); michael@0: getParameters().put("uri", request.getRequestLine().getUri()); michael@0: String charset = getParameter("charset"); michael@0: if (charset == null) { michael@0: charset = AuthParams.getCredentialCharset(request.getParams()); michael@0: getParameters().put("charset", charset); michael@0: } michael@0: return createDigestHeader(credentials); michael@0: } michael@0: michael@0: private static MessageDigest createMessageDigest( michael@0: final String digAlg) throws UnsupportedDigestAlgorithmException { michael@0: try { michael@0: return MessageDigest.getInstance(digAlg); michael@0: } catch (Exception e) { michael@0: throw new UnsupportedDigestAlgorithmException( michael@0: "Unsupported algorithm in HTTP Digest authentication: " michael@0: + digAlg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Creates digest-response header as defined in RFC2617. michael@0: * michael@0: * @param credentials User credentials michael@0: * michael@0: * @return The digest-response as String. michael@0: */ michael@0: private Header createDigestHeader( michael@0: final Credentials credentials) throws AuthenticationException { michael@0: String uri = getParameter("uri"); michael@0: String realm = getParameter("realm"); michael@0: String nonce = getParameter("nonce"); michael@0: String opaque = getParameter("opaque"); michael@0: String method = getParameter("methodname"); michael@0: String algorithm = getParameter("algorithm"); michael@0: if (uri == null) { michael@0: throw new IllegalStateException("URI may not be null"); michael@0: } michael@0: if (realm == null) { michael@0: throw new IllegalStateException("Realm may not be null"); michael@0: } michael@0: if (nonce == null) { michael@0: throw new IllegalStateException("Nonce may not be null"); michael@0: } michael@0: michael@0: //TODO: add support for QOP_INT michael@0: int qop = QOP_UNKNOWN; michael@0: String qoplist = getParameter("qop"); michael@0: if (qoplist != null) { michael@0: StringTokenizer tok = new StringTokenizer(qoplist, ","); michael@0: while (tok.hasMoreTokens()) { michael@0: String variant = tok.nextToken().trim(); michael@0: if (variant.equals("auth")) { michael@0: qop = QOP_AUTH; michael@0: break; michael@0: } michael@0: } michael@0: } else { michael@0: qop = QOP_MISSING; michael@0: } michael@0: michael@0: if (qop == QOP_UNKNOWN) { michael@0: throw new AuthenticationException("None of the qop methods is supported: " + qoplist); michael@0: } michael@0: michael@0: // If an algorithm is not specified, default to MD5. michael@0: if (algorithm == null) { michael@0: algorithm = "MD5"; michael@0: } michael@0: // If an charset is not specified, default to ISO-8859-1. michael@0: String charset = getParameter("charset"); michael@0: if (charset == null) { michael@0: charset = "ISO-8859-1"; michael@0: } michael@0: michael@0: String digAlg = algorithm; michael@0: if (digAlg.equalsIgnoreCase("MD5-sess")) { michael@0: digAlg = "MD5"; michael@0: } michael@0: michael@0: MessageDigest digester; michael@0: try { michael@0: digester = createMessageDigest(digAlg); michael@0: } catch (UnsupportedDigestAlgorithmException ex) { michael@0: throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg); michael@0: } michael@0: michael@0: String uname = credentials.getUserPrincipal().getName(); michael@0: String pwd = credentials.getPassword(); michael@0: michael@0: if (nonce.equals(this.lastNonce)) { michael@0: nounceCount++; michael@0: } else { michael@0: nounceCount = 1; michael@0: cnonce = null; michael@0: lastNonce = nonce; michael@0: } michael@0: StringBuilder sb = new StringBuilder(256); michael@0: Formatter formatter = new Formatter(sb, Locale.US); michael@0: formatter.format("%08x", nounceCount); michael@0: String nc = sb.toString(); michael@0: michael@0: if (cnonce == null) { michael@0: cnonce = createCnonce(); michael@0: } michael@0: michael@0: a1 = null; michael@0: a2 = null; michael@0: // 3.2.2.2: Calculating digest michael@0: if (algorithm.equalsIgnoreCase("MD5-sess")) { michael@0: // H( unq(username-value) ":" unq(realm-value) ":" passwd ) michael@0: // ":" unq(nonce-value) michael@0: // ":" unq(cnonce-value) michael@0: michael@0: // calculated one per session michael@0: sb.setLength(0); michael@0: sb.append(uname).append(':').append(realm).append(':').append(pwd); michael@0: String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset))); michael@0: sb.setLength(0); michael@0: sb.append(checksum).append(':').append(nonce).append(':').append(cnonce); michael@0: a1 = sb.toString(); michael@0: } else { michael@0: // unq(username-value) ":" unq(realm-value) ":" passwd michael@0: sb.setLength(0); michael@0: sb.append(uname).append(':').append(realm).append(':').append(pwd); michael@0: a1 = sb.toString(); michael@0: } michael@0: michael@0: String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset))); michael@0: michael@0: if (qop == QOP_AUTH) { michael@0: // Method ":" digest-uri-value michael@0: a2 = method + ':' + uri; michael@0: } else if (qop == QOP_AUTH_INT) { michael@0: // Method ":" digest-uri-value ":" H(entity-body) michael@0: //TODO: calculate entity hash if entity is repeatable michael@0: throw new AuthenticationException("qop-int method is not suppported"); michael@0: } else { michael@0: a2 = method + ':' + uri; michael@0: } michael@0: michael@0: String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset))); michael@0: michael@0: // 3.2.2.1 michael@0: michael@0: String digestValue; michael@0: if (qop == QOP_MISSING) { michael@0: sb.setLength(0); michael@0: sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2); michael@0: digestValue = sb.toString(); michael@0: } else { michael@0: sb.setLength(0); michael@0: sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':') michael@0: .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth") michael@0: .append(':').append(hasha2); michael@0: digestValue = sb.toString(); michael@0: } michael@0: michael@0: String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue))); michael@0: michael@0: CharArrayBuffer buffer = new CharArrayBuffer(128); michael@0: if (isProxy()) { michael@0: buffer.append(AUTH.PROXY_AUTH_RESP); michael@0: } else { michael@0: buffer.append(AUTH.WWW_AUTH_RESP); michael@0: } michael@0: buffer.append(": Digest "); michael@0: michael@0: List params = new ArrayList(20); michael@0: params.add(new BasicNameValuePair("username", uname)); michael@0: params.add(new BasicNameValuePair("realm", realm)); michael@0: params.add(new BasicNameValuePair("nonce", nonce)); michael@0: params.add(new BasicNameValuePair("uri", uri)); michael@0: params.add(new BasicNameValuePair("response", digest)); michael@0: michael@0: if (qop != QOP_MISSING) { michael@0: params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth")); michael@0: params.add(new BasicNameValuePair("nc", nc)); michael@0: params.add(new BasicNameValuePair("cnonce", cnonce)); michael@0: } michael@0: if (algorithm != null) { michael@0: params.add(new BasicNameValuePair("algorithm", algorithm)); michael@0: } michael@0: if (opaque != null) { michael@0: params.add(new BasicNameValuePair("opaque", opaque)); michael@0: } michael@0: michael@0: for (int i = 0; i < params.size(); i++) { michael@0: BasicNameValuePair param = params.get(i); michael@0: if (i > 0) { michael@0: buffer.append(", "); michael@0: } michael@0: boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName()); michael@0: BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes); michael@0: } michael@0: return new BufferedHeader(buffer); michael@0: } michael@0: michael@0: String getCnonce() { michael@0: return cnonce; michael@0: } michael@0: michael@0: String getA1() { michael@0: return a1; michael@0: } michael@0: michael@0: String getA2() { michael@0: return a2; michael@0: } michael@0: michael@0: /** michael@0: * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long michael@0: * String according to RFC 2617. michael@0: * michael@0: * @param binaryData array containing the digest michael@0: * @return encoded MD5, or null if encoding failed michael@0: */ michael@0: private static String encode(byte[] binaryData) { michael@0: int n = binaryData.length; michael@0: char[] buffer = new char[n * 2]; michael@0: for (int i = 0; i < n; i++) { michael@0: int low = (binaryData[i] & 0x0f); michael@0: int high = ((binaryData[i] & 0xf0) >> 4); michael@0: buffer[i * 2] = HEXADECIMAL[high]; michael@0: buffer[(i * 2) + 1] = HEXADECIMAL[low]; michael@0: } michael@0: michael@0: return new String(buffer); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Creates a random cnonce value based on the current time. michael@0: * michael@0: * @return The cnonce value as String. michael@0: */ michael@0: public static String createCnonce() { michael@0: SecureRandom rnd = new SecureRandom(); michael@0: byte[] tmp = new byte[8]; michael@0: rnd.nextBytes(tmp); michael@0: return encode(tmp); michael@0: } michael@0: michael@0: }