at.bitfire.dav4android.BasicDigestAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for at.bitfire.dav4android.BasicDigestAuthenticator.java

Source

/*
 * Copyright  2013  2015 Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */

package at.bitfire.dav4android;

import android.text.TextUtils;

import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

import java.io.IOException;
import java.net.Proxy;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import lombok.NonNull;
import okio.Buffer;
import okio.ByteString;

public class BasicDigestAuthenticator implements Authenticator {
    protected static final String HEADER_AUTHENTICATE = "WWW-Authenticate", HEADER_AUTHORIZATION = "Authorization";

    final String host, username, password;

    final String clientNonce;
    AtomicInteger nonceCount = new AtomicInteger(1);

    public BasicDigestAuthenticator(String host, String username, String password) {
        this.host = host;
        this.username = username;
        this.password = password;

        clientNonce = h(UUID.randomUUID().toString());
    }

    BasicDigestAuthenticator(String host, String username, String password, String clientNonce) {
        this.host = null;
        this.username = username;
        this.password = password;
        this.clientNonce = clientNonce;
    }

    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {
        Request request = response.request();

        if (host != null && !request.httpUrl().host().equalsIgnoreCase(host)) {
            Constants.log.warn("Not authenticating against " + host + " for security reasons!");
            return null;
        }

        // check whether this is the first authentication try with our credentials
        Response priorResponse = response.priorResponse();
        boolean triedBefore = priorResponse != null ? priorResponse.request().header(HEADER_AUTHORIZATION) != null
                : false;

        HttpUtils.AuthScheme basicAuth = null, digestAuth = null;
        for (HttpUtils.AuthScheme scheme : HttpUtils
                .parseWwwAuthenticate(response.headers(HEADER_AUTHENTICATE).toArray(new String[0])))
            if ("Basic".equalsIgnoreCase(scheme.name))
                basicAuth = scheme;
            else if ("Digest".equalsIgnoreCase(scheme.name))
                digestAuth = scheme;

        // we MUST prefer Digest auth [https://tools.ietf.org/html/rfc2617#section-4.6]
        if (digestAuth != null) {
            // Digest auth

            if (triedBefore && !"true".equalsIgnoreCase(digestAuth.params.get("stale")))
                // credentials didn't work last time, and they won't work now -> stop here
                return null;
            return authorizationRequest(request, digestAuth);

        } else if (basicAuth != null) {
            // Basic auth
            if (triedBefore) // credentials didn't work last time, and they won't work now -> stop here
                return null;

            return request.newBuilder().header(HEADER_AUTHORIZATION, Credentials.basic(username, password)).build();
        }

        // no supported auth scheme
        return null;
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        return null;
    }

    protected Request authorizationRequest(Request request, HttpUtils.AuthScheme digest) {
        String realm = digest.params.get("realm"), opaque = digest.params.get("opaque"),
                nonce = digest.params.get("nonce");

        Algorithm algorithm = Algorithm.determine(digest.params.get("algorithm"));
        Protection qop = Protection.selectFrom(digest.params.get("qop"));

        // build response parameters
        String response = null;

        List<String> params = new LinkedList<>();
        params.add("username=" + quotedString(username));
        if (realm != null)
            params.add("realm=" + quotedString(realm));
        else
            return null;
        if (nonce != null)
            params.add("nonce=" + quotedString(nonce));
        else
            return null;
        if (opaque != null)
            params.add("opaque=" + quotedString(opaque));
        else
            return null;

        if (algorithm != null)
            params.add("algorithm=" + quotedString(algorithm.name));

        final String method = request.method();
        final String digestURI = request.httpUrl().encodedPath();
        params.add("uri=" + quotedString(digestURI));

        if (qop != null) {
            params.add("qop=" + qop.name);
            params.add("cnonce=" + quotedString(clientNonce));

            int nc = nonceCount.getAndIncrement();
            String ncValue = String.format("%08x", nc);
            params.add("nc=" + ncValue);

            String a1 = null;
            if (algorithm == Algorithm.MD5)
                a1 = username + ":" + realm + ":" + password;
            else if (algorithm == Algorithm.MD5_SESSION)
                a1 = h(username + ":" + realm + ":" + password) + ":" + nonce + ":" + clientNonce;
            //Constants.log.trace("A1=" + a1);

            String a2 = null;
            if (qop == Protection.Auth)
                a2 = method + ":" + digestURI;
            else if (qop == Protection.AuthInt)
                try {
                    RequestBody body = request.body();
                    a2 = method + ":" + digestURI + ":" + (body != null ? h(body) : h(""));
                } catch (IOException e) {
                    Constants.log.warn("Couldn't get entity-body for hash calculation");
                }
            //Constants.log.trace("A2=" + a2);

            if (a1 != null && a2 != null)
                response = kd(h(a1), nonce + ":" + ncValue + ":" + clientNonce + ":" + qop.name + ":" + h(a2));

        } else {
            //Constants.log.debug("Using legacy Digest auth");

            // legacy (backwards compatibility with RFC 2069)
            if (algorithm == Algorithm.MD5) {
                String a1 = username + ":" + realm + ":" + password, a2 = method + ":" + digestURI;
                response = kd(h(a1), nonce + ":" + h(a2));
            }
        }

        if (response != null) {
            params.add("response=" + quotedString(response));
            return request.newBuilder().header(HEADER_AUTHORIZATION, "Digest " + TextUtils.join(", ", params))
                    .build();
        } else
            return null;
    }

    protected String quotedString(String s) {
        return "\"" + s.replace("\"", "\\\"") + "\"";
    }

    protected String h(String data) {
        return ByteString.of(data.getBytes()).md5().hex();
    }

    protected String h(@NonNull RequestBody body) throws IOException {
        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        return ByteString.of(buffer.readByteArray()).md5().hex();
    }

    protected String kd(String secret, String data) {
        return h(secret + ":" + data);
    }

    protected enum Algorithm {
        MD5("MD5"), MD5_SESSION("MD5-sess");

        public final String name;

        Algorithm(String name) {
            this.name = name;
        }

        static Algorithm determine(String paramValue) {
            if (paramValue == null || Algorithm.MD5.name.equalsIgnoreCase(paramValue))
                return Algorithm.MD5;
            else if (Algorithm.MD5_SESSION.name.equals(paramValue))
                return Algorithm.MD5_SESSION;
            else
                Constants.log.warn("Ignoring unknown hash algorithm: " + paramValue);
            return null;
        }
    }

    protected enum Protection { // quality of protection:
        Auth("auth"), // authentication only
        AuthInt("auth-int"); // authentication with integrity protection

        public final String name;

        Protection(String name) {
            this.name = name;
        }

        static Protection selectFrom(String paramValue) {
            if (paramValue != null) {
                boolean qopAuth = false, qopAuthInt = false;
                for (String qop : paramValue.split(","))
                    if ("auth".equals(qop))
                        qopAuth = true;
                    else if ("auth-int".equals(qop))
                        qopAuthInt = true;

                // prefer auth-int as it provides more protection
                if (qopAuthInt)
                    return AuthInt;
                else if (qopAuth)
                    return Auth;
            }
            return null;
        }
    }

}