<commit_before>/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.domain.http.server.security;

import static org.jboss.as.domain.http.server.Constants.AUTHORIZATION_HEADER;
import static org.jboss.as.domain.http.server.Constants.FORBIDDEN;
import static org.jboss.as.domain.http.server.Constants.GET;
import static org.jboss.as.domain.http.server.Constants.UNAUTHORIZED;
import static org.jboss.as.domain.http.server.Constants.WWW_AUTHENTICATE_HEADER;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.RealmCallback;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import org.jboss.as.domain.management.util.HexUtil;
import org.jboss.com.sun.net.httpserver.Authenticator;
import org.jboss.com.sun.net.httpserver.Headers;
import org.jboss.com.sun.net.httpserver.HttpExchange;
import org.jboss.com.sun.net.httpserver.HttpPrincipal;

/**
 * An authenticator to handle Digest authentication.
 *
 * @author <a href="mailto:<EMAIL>"><NAME></a>
 */
public class DigestAuthenticator extends Authenticator {

    private final NonceFactory nonceFactory = new NonceFactory();

    private final CallbackHandler callbackHandler;

    private final String realm;

    private static final byte COLON = ':';
    private static final String CHALLENGE = "Digest";
    private static final String NONCE = "nonce";
    private static final String MD5 = "MD5";
    private static final String REALM = "realm";
    private static final String RESPONSE = "response";
    private static final String USERNAME = "username";
    private static final String URI = "uri";

    public DigestAuthenticator(CallbackHandler callbackHandler, String realm) {
        this.callbackHandler = callbackHandler;
        this.realm = realm;
    }

    @Override
    public Result authenticate(HttpExchange httpExchange) {
        // If authentication has already completed for this connection re-use it.
        DigestContext context = getOrCreateNegotiationContext(httpExchange);
        if (context.isAuthenticated()) {
            return new Authenticator.Success(context.getPrincipal());
        }

        // No previous authentication so time to continue the process.
        Headers requestHeaders = httpExchange.getRequestHeaders();
        if (requestHeaders.containsKey(AUTHORIZATION_HEADER) == false) {
            Headers responseHeaders = httpExchange.getResponseHeaders();

            responseHeaders.add(WWW_AUTHENTICATE_HEADER, CHALLENGE + " " + createChallenge(false));

            return new Authenticator.Retry(UNAUTHORIZED);
        }

        String authorizationHeader = requestHeaders.getFirst(AUTHORIZATION_HEADER);
        if (authorizationHeader.startsWith(CHALLENGE + " ") == false) {
            throw new RuntimeException("Invalid 'Authorization' header.");
        }
        String challenge = authorizationHeader.substring(CHALLENGE.length() + 1);
        Map<String, String> challengeParameters = parseDigestChallenge(challenge);

        // Validate Challenge, expect one of 3 responses VALID, INVALID, STALE
        HttpPrincipal principal = validateUser(challengeParameters);
        // INVALID - Username / Password verification failed - Nonce is irelevant.
        if (principal == null) {
            if (challengeParameters.containsKey(NONCE)) {
                nonceFactory.useNonce(challengeParameters.get(NONCE));
            }
            return new Failure(FORBIDDEN);
        }

        // VALID - Verified username and password, Nonce is correct.
        if (nonceFactory.useNonce(challengeParameters.get(NONCE))) {
            context.principal = principal;

            return new Authenticator.Success(principal);
        }

        // STALE - Verification of username and password succeeded but Nonce now stale.
        Headers responseHeaders = httpExchange.getResponseHeaders();
        responseHeaders.add(WWW_AUTHENTICATE_HEADER, CHALLENGE + " " + createChallenge(true));
        return new Authenticator.Retry(UNAUTHORIZED);
    }

    private HttpPrincipal validateUser(Map<String, String> challengeParameters) {
        // Step 1 - Create Callbacks
        // TODO - Should we use SASL callbacks or add our own and have our handler support both?
        /* TODO - Maybe also consider callbacks for returning some parts ready hashed e.g. HA1 - this
           would mean password not even needed by JBoss installation, Hash would also only be applicable
           this realm. */
        RealmCallback rcb = new RealmCallback("Realm", challengeParameters.get(REALM));
        NameCallback ncb = new NameCallback("Username", challengeParameters.get(USERNAME));
        PasswordCallback pcb = new PasswordCallback("Password", false);
        Callback[] callbacks = new Callback[]{rcb, ncb, pcb};

        // Step 2 - Call CallbackHandler
        try {
            callbackHandler.handle(callbacks);
        } catch (IOException e) {
            throw new IllegalStateException("CallbackHander not suitable for Digest authentication.");
        } catch (UnsupportedCallbackException e) {
            throw new IllegalStateException("CallbackHander not suitable for Digest authentication.");
        }

        // TODO - Verify that a password was set (Depending on if multiple CallbackHandlers are supported)

        // Step 3 - Generate MD5 and Compare
        try {
            // TODO - The remaining combinations from RFC-2617 need to be added.
            // TODO - Verify all required parameters were set.
            MessageDigest md = MessageDigest.getInstance(MD5);
            md.update(challengeParameters.get(USERNAME).getBytes());
            md.update(COLON);
            md.update(challengeParameters.get(REALM).getBytes());
            md.update(COLON);
            md.update(new String(pcb.getPassword()).getBytes());

            byte[] ha1 = HexUtil.convertToHexBytes(md.digest());

            md.update(GET.getBytes());
            md.update(COLON);
            md.update(challengeParameters.get(URI).getBytes());

            byte[] ha2 = HexUtil.convertToHexBytes(md.digest());

            md.update(ha1);
            md.update(COLON);
            md.update(challengeParameters.get(NONCE).getBytes());
            md.update(COLON);
            md.update(ha2);

            byte[] expectedResponse = HexUtil.convertToHexBytes(md.digest());
            byte[] actualResponse = challengeParameters.get(RESPONSE).getBytes();

            if (MessageDigest.isEqual(expectedResponse, actualResponse)) {
                return new HttpPrincipal(challengeParameters.get(USERNAME), challengeParameters.get(REALM));
            }

        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Unable to perform digest validation as MD5 is unavailable.", e);
        }

        return null;
    }


    private String createChallenge(boolean stale) {
        StringBuilder challenge = new StringBuilder();
        challenge.append("realm=\"").append(realm).append("\",");
        challenge.append("nonce=\"").append(nonceFactory.createNonce()).append("\"");<commit_msg>Fix AS7-881<commit_after>/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.domain.http.server.security;

import static org.jboss.as.domain.http.server.Constants.AUTHORIZATION_HEADER;
import static org.jboss.as.domain.http.server.Constants.FORBIDDEN;
import static org.jboss.as.domain.http.server.Constants.GET;
import static org.jboss.as.domain.http.server.Constants.UNAUTHORIZED;
import static org.jboss.as.domain.http.server.Constants.WWW_AUTHENTICATE_HEADER;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.RealmCallback;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import org.jboss.as.domain.management.util.HexUtil;
import org.jboss.com.sun.net.httpserver.Authenticator;
import org.jboss.com.sun.net.httpserver.Headers;
import org.jboss.com.sun.net.httpserver.HttpExchange;
import org.jboss.com.sun.net.httpserver.HttpPrincipal;

/**
 * An authenticator to handle Digest authentication.
 *
 * @author <a href="mailto:<EMAIL>"><NAME></a>
 */
public class DigestAuthenticator extends Authenticator {

    private final NonceFactory nonceFactory = new NonceFactory();

    private final CallbackHandler callbackHandler;

    private final String realm;

    private static final byte COLON = ':';
    private static final String CHALLENGE = "Digest";
    private static final String NONCE = "nonce";
    private static final String MD5 = "MD5";
    private static final String REALM = "realm";
    private static final String RESPONSE = "response";
    private static final String USERNAME = "username";
    private static final String URI = "uri";

    public DigestAuthenticator(CallbackHandler callbackHandler, String realm) {
        this.callbackHandler = callbackHandler;
        this.realm = realm;
    }

    @Override
    public Result authenticate(HttpExchange httpExchange) {
        // If authentication has already completed for this connection re-use it.
        DigestContext context = getOrCreateNegotiationContext(httpExchange);
        if (context.isAuthenticated()) {
            return new Authenticator.Success(context.getPrincipal());
        }

        // No previous authentication so time to continue the process.
        Headers requestHeaders = httpExchange.getRequestHeaders();
        if (requestHeaders.containsKey(AUTHORIZATION_HEADER) == false) {
            Headers responseHeaders = httpExchange.getResponseHeaders();

            responseHeaders.add(WWW_AUTHENTICATE_HEADER, CHALLENGE + " " + createChallenge(false));

            return new Authenticator.Retry(UNAUTHORIZED);
        }

        String authorizationHeader = requestHeaders.getFirst(AUTHORIZATION_HEADER);
        if (authorizationHeader.startsWith(CHALLENGE + " ") == false) {
            throw new RuntimeException("Invalid 'Authorization' header.");
        }
        String challenge = authorizationHeader.substring(CHALLENGE.length() + 1);
        Map<String, String> challengeParameters = parseDigestChallenge(challenge);

        // Validate Challenge, expect one of 3 responses VALID, INVALID, STALE
        HttpPrincipal principal = validateUser(httpExchange, challengeParameters);
        // INVALID - Username / Password verification failed - Nonce is irelevant.
        if (principal == null) {
            if (challengeParameters.containsKey(NONCE)) {
                nonceFactory.useNonce(challengeParameters.get(NONCE));
            }
            return new Failure(FORBIDDEN);
        }

        // VALID - Verified username and password, Nonce is correct.
        if (nonceFactory.useNonce(challengeParameters.get(NONCE))) {
            context.principal = principal;

            return new Authenticator.Success(principal);
        }

        // STALE - Verification of username and password succeeded but Nonce now stale.
        Headers responseHeaders = httpExchange.getResponseHeaders();
        responseHeaders.add(WWW_AUTHENTICATE_HEADER, CHALLENGE + " " + createChallenge(true));
        return new Authenticator.Retry(UNAUTHORIZED);
    }

    private HttpPrincipal validateUser(HttpExchange httpExchange, Map<String, String> challengeParameters) {
        // Step 1 - Create Callbacks
        // TODO - Should we use SASL callbacks or add our own and have our handler support both?
        /* TODO - Maybe also consider callbacks for returning some parts ready hashed e.g. HA1 - this
           would mean password not even needed by JBoss installation, Hash would also only be applicable
           this realm. */
        RealmCallback rcb = new RealmCallback("Realm", challengeParameters.get(REALM));
        NameCallback ncb = new NameCallback("Username", challengeParameters.get(USERNAME));
        PasswordCallback pcb = new PasswordCallback("Password", false);
        Callback[] callbacks = new Callback[]{rcb, ncb, pcb};

        // Step 2 - Call CallbackHandler
        try {
            callbackHandler.handle(callbacks);
        } catch (IOException e) {
            throw new IllegalStateException("CallbackHander not suitable for Digest authentication.");
        } catch (UnsupportedCallbackException e) {
            throw new IllegalStateException("CallbackHander not suitable for Digest authentication.");
        }

        // TODO - Verify that a password was set (Depending on if multiple CallbackHandlers are supported)

        // Step 3 - Generate MD5 and Compare
        try {
            // TODO - The remaining combinations from RFC-2617 need to be added.
            // TODO - Verify all required parameters were set.
            MessageDigest md = MessageDigest.getInstance(MD5);
            md.update(challengeParameters.get(USERNAME).getBytes());
            md.update(COLON);
            md.update(challengeParameters.get(REALM).getBytes());
            md.update(COLON);
            md.update(new String(pcb.getPassword()).getBytes());

            byte[] ha1 = HexUtil.convertToHexBytes(md.digest());

            md.update(httpExchange.getRequestMethod().getBytes());
            md.update(COLON);
            md.update(challengeParameters.get(URI).getBytes());

            byte[] ha2 = HexUtil.convertToHexBytes(md.digest());

            md.update(ha1);
            md.update(COLON);
            md.update(challengeParameters.get(NONCE).getBytes());
            md.update(COLON);
            md.update(ha2);

            byte[] expectedResponse = HexUtil.convertToHexBytes(md.digest());
            byte[] actualResponse = challengeParameters.get(RESPONSE).getBytes();

            if (MessageDigest.isEqual(expectedResponse, actualResponse)) {
                return new HttpPrincipal(challengeParameters.get(USERNAME), challengeParameters.get(REALM));
            }

        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Unable to perform digest validation as MD5 is unavailable.", e);
        }

        return null;
    }


    private String createChallenge(boolean stale) {
        StringBuilder challenge = new StringBuilder();
        challenge.append("realm=\"").append(realm).append("\",");
        challenge.append("nonce=\"").append(nonceFactory.createNonce()).append("\"");
{'fraction_non_alphanumeric': 0.09229672744550692, 'fraction_numerical': 0.004513868266318228, 'mean_word_length': 3.1626699629171817, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7492678', 'n_tokens_mistral': 4518, 'n_tokens_neox': 4287, 'n_words': 1255}