All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wildfly.security.sasl.digest.DigestSaslServer Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote Jakarta Enterprise Beans and Jakarta Messaging, including all dependencies. It is intended for use by those not using maven, maven users should just import the Jakarta Enterprise Beans and Jakarta Messaging BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

The newest version!
/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wildfly.security.sasl.digest;

import static org.wildfly.security.mechanism._private.ElytronMessages.saslDigest;
import static org.wildfly.security.mechanism.digest.DigestUtil.parseResponse;
import static org.wildfly.security.sasl.digest._private.DigestUtil.H_A1;
import static org.wildfly.security.sasl.digest._private.DigestUtil.QOP_AUTH;
import static org.wildfly.security.sasl.digest._private.DigestUtil.QOP_AUTH_CONF;
import static org.wildfly.security.sasl.digest._private.DigestUtil.QOP_VALUES;
import static org.wildfly.security.sasl.digest._private.DigestUtil.digestResponse;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Provider;
import java.util.HashMap;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.function.Supplier;

import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;

import org.wildfly.common.Assert;
import org.wildfly.common.bytes.ByteStringBuilder;
import org.wildfly.security.mechanism.AuthenticationMechanismException;
import org.wildfly.security.mechanism.digest.DigestQuote;

/**
 * A server implementation of RFC 2831 {@code DIGEST} SASL mechanism.
 *
 * @author Peter Skopek
 */
final class DigestSaslServer extends AbstractDigestMechanism implements SaslServer {

    private final Predicate digestUriProtocolAccepted;
    private final boolean defaultRealm; // realms not defined, server name used as fallback

    DigestSaslServer(String[] realms, final boolean defaultRealm, String mechanismName, String protocol, String serverName, CallbackHandler callbackHandler, Charset charset, String[] qops, String[] ciphers, Predicate digestUriProtocolAccepted, Supplier providers) throws SaslException {
        super(mechanismName, protocol, serverName, callbackHandler, FORMAT.SERVER, charset, ciphers, providers);
        this.realms = realms;
        this.defaultRealm = defaultRealm;
        this.supportedCiphers = getSupportedCiphers(ciphers);
        this.qops = qops;
        this.digestUriProtocolAccepted = digestUriProtocolAccepted;
    }

    private static final byte STEP_ONE = 1;
    private static final byte STEP_THREE = 3;

    private String[] realms;
    private String supportedCiphers;
    private int receivingMaxBuffSize = DEFAULT_MAXBUF;
    private String[] qops;
    private int nonceCount = -1;
    private String receivedClientUri;
    private String boundServerName = null;

    /**
     * Generates a digest challenge
     *
     *    digest-challenge  =
     *    1#( realm | nonce | qop-options | stale | maxbuf | charset
     *          algorithm | cipher-opts | auth-param )
     *
     *   realm             = "realm" "=" <"> realm-value <">
     *   realm-value       = qdstr-val
     *   nonce             = "nonce" "=" <"> nonce-value <">
     *   nonce-value       = qdstr-val
     *   qop-options       = "qop" "=" <"> qop-list <">
     *   qop-list          = 1#qop-value
     *   qop-value         = "auth" | "auth-int" | "auth-conf" |
     *                        token
     *   stale             = "stale" "=" "true"
     *   maxbuf            = "maxbuf" "=" maxbuf-value
     *   maxbuf-value      = 1*DIGIT
     *   charset           = "charset" "=" "utf-8"
     *   algorithm         = "algorithm" "=" "md5-sess"
     *   cipher-opts       = "cipher" "=" <"> 1#cipher-value <">
     *   cipher-value      = "3des" | "des" | "rc4-40" | "rc4" |
     *                       "rc4-56" | token
     *   auth-param        = token "=" ( token | quoted-string )
     * @return
     */
    private byte[] generateChallenge() {
        ByteStringBuilder challenge = new ByteStringBuilder();

        // realms
        StringBuilder sb = new StringBuilder();
        for (String realm: this.realms) {
            sb.append("realm=\"").append(DigestQuote.quote(realm)).append("\"").append(DELIMITER);
        }
        challenge.append(sb.toString().getBytes(getCharset()));


        // nonce
        assert nonce == null;
        nonce = generateNonce();
        challenge.append("nonce=\"");
        challenge.append(DigestQuote.quote(nonce));
        challenge.append("\"").append(DELIMITER);

        // qop
        if (qops != null) {
            challenge.append("qop=\"");
            boolean first = true;
            for(String qop : qops){
                if(!first) challenge.append(DELIMITER);
                first = false;
                challenge.append(DigestQuote.quote(qop));
            }
            challenge.append("\"").append(DELIMITER);
        }

        // maxbuf
        if (receivingMaxBuffSize != DEFAULT_MAXBUF) {
            challenge.append("maxbuf=");
            challenge.append(String.valueOf(receivingMaxBuffSize));
            challenge.append(DELIMITER);
        }

        // charset
        if (StandardCharsets.UTF_8.equals(getCharset())) {
            challenge.append("charset=");
            challenge.append("utf-8");
            challenge.append(DELIMITER);
        }

        // cipher
        if (supportedCiphers != null && qops != null && arrayContains(qops, QOP_AUTH_CONF)) {
            challenge.append("cipher=\"");
            challenge.append(supportedCiphers);
            challenge.append("\"").append(DELIMITER);
        }

        challenge.append("algorithm=md5-sess"); // only for backwards compatibility with HTTP Digest

        return challenge.toArray();
    }

    private void noteDigestResponseData(HashMap parsedDigestResponse) {
        byte[] data = parsedDigestResponse.get("nc");
        if (data != null) {
            nonceCount = Integer.parseInt(new String(data, StandardCharsets.UTF_8));
        } else {
            nonceCount = -1;
        }

        data = parsedDigestResponse.get("cipher");
        if (data != null) {
            cipher = new String(data, StandardCharsets.UTF_8);
        } else {
            cipher = "";
        }

        data = parsedDigestResponse.get("authzid");
        if (data != null) {
            authorizationId = new String(data, StandardCharsets.UTF_8);
        } else {
            authorizationId = null;
        }
    }

    private byte[] validateDigestResponse(HashMap parsedDigestResponse) throws SaslException {
        if (nonceCount != 1) {
            throw saslDigest.mechNonceCountMustEqual(1, nonceCount).toSaslException();
        }

        Charset clientCharset = StandardCharsets.ISO_8859_1;
        if (parsedDigestResponse.get("charset") != null) {
            String cCharset = new String(parsedDigestResponse.get("charset"), StandardCharsets.UTF_8);
            if (cCharset.equals("utf-8")) {
                if (StandardCharsets.UTF_8.equals(getCharset())) {
                    clientCharset = StandardCharsets.UTF_8;
                } else {
                    throw saslDigest.mechUnsupportedCharset("UTF-8").toSaslException();
                }
            } else {
                throw saslDigest.mechUnknownCharset().toSaslException();
            }
        }

        if (parsedDigestResponse.get("username") != null) {
            username = new String(parsedDigestResponse.get("username"), clientCharset);
        } else {
            throw saslDigest.mechMissingDirective("username").toSaslException();
        }

        if (parsedDigestResponse.get("realm") != null) {
            realm = new String(parsedDigestResponse.get("realm"), clientCharset);
        } else {
            realm = "";
        }
        if (!arrayContains(realms, realm)) {
            throw saslDigest.mechDisallowedClientRealm(realm).toSaslException();
        }

        if (parsedDigestResponse.get("nonce") == null) {
            throw saslDigest.mechMissingDirective("nonce").toSaslException();
        }

        byte[] nonceFromClient = parsedDigestResponse.get("nonce");
        if (! MessageDigest.isEqual(nonce, nonceFromClient)) {
            throw saslDigest.mechNoncesDoNotMatch().toSaslException();
        }

        if (parsedDigestResponse.get("cnonce") == null) {
            throw saslDigest.mechMissingDirective("cnonce").toSaslException();
        }
        cnonce = parsedDigestResponse.get("cnonce");

        if (parsedDigestResponse.get("nc") == null) {
            throw saslDigest.mechMissingDirective("nc").toSaslException();
        }

        if (parsedDigestResponse.get("digest-uri") != null) {
            receivedClientUri = new String(parsedDigestResponse.get("digest-uri"), clientCharset);
            String[] parts = receivedClientUri.split("/", 2);
            String expectedServerName = getServerName();

            if (! digestUriProtocolAccepted.test(parts[0].toLowerCase(Locale.ROOT)) ||
                (expectedServerName != null && ! expectedServerName.toLowerCase(Locale.ROOT).equals(parts[1].toLowerCase(Locale.ROOT)))
               ) {
                throw saslDigest.mechMismatchedWrongDigestUri(receivedClientUri).toSaslException();
            }

            boundServerName = parts[1];
        } else {
            throw saslDigest.mechMissingDirective("digest-uri").toSaslException();
        }

        qop = QOP_AUTH;
        if (parsedDigestResponse.get("qop") != null) {
            qop = new String(parsedDigestResponse.get("qop"), clientCharset);
            if (!arrayContains(QOP_VALUES, qop)) {
                throw saslDigest.mechUnexpectedQop(qop).toSaslException();
            }
            if (qop != null && qop.equals(QOP_AUTH) == false) {
                setWrapper(new DigestWrapper(qop.equals(QOP_AUTH_CONF)));
            }
        }

        byte[] digest_urp = handleUserRealmPasswordCallbacks(null, true, defaultRealm);
        hA1 = H_A1(messageDigest, digest_urp, nonce, cnonce, authorizationId, clientCharset);

        byte[] expectedResponse = digestResponse(messageDigest, hA1, nonce, nonceCount, cnonce, authorizationId, qop, receivedClientUri, true);

        // check response
        if (parsedDigestResponse.get("response") == null) {
            throw saslDigest.mechMissingDirective("response").toSaslException();
        }
        if (! MessageDigest.isEqual(expectedResponse, parsedDigestResponse.get("response"))) {
            throw saslDigest.mechAuthenticationRejectedInvalidProof().toSaslException();
        }

        createCiphersAndKeys();

        // authorization check
        String authzid = (authorizationId == null || authorizationId.isEmpty()) ? username : authorizationId;
        final AuthorizeCallback authorizeCallback = new AuthorizeCallback(username, authzid);
        try {
            tryHandleCallbacks(authorizeCallback);
        } catch (UnsupportedCallbackException e) {
            throw saslDigest.mechAuthorizationUnsupported(e).toSaslException();
        }
        if (authorizeCallback.isAuthorized()) {
            authorizationId = authorizeCallback.getAuthorizedID();
        } else {
            throw saslDigest.mechAuthorizationFailed(username, authzid).toSaslException();
        }

        return createResponseAuth();
    }

    private byte[] createResponseAuth() {
        ByteStringBuilder responseAuth = new ByteStringBuilder();
        responseAuth.append("rspauth=");

        byte[] response_value = digestResponse(messageDigest, hA1, nonce, nonceCount, cnonce, authorizationId, qop, receivedClientUri != null ? receivedClientUri : digestURI, false);

        responseAuth.append(response_value);
        return responseAuth.toArray();
    }

    /* (non-Javadoc)
     * @see javax.security.sasl.SaslServer#getAuthorizationID()
     */
    @Override
    public String getAuthorizationID() {
        return authorizationId;
    }

    /* (non-Javadoc)
     * @see javax.security.sasl.SaslServer#getNegotiatedProperty(String)
     */
    @Override
    public Object getNegotiatedProperty(final String propName) {
        assertComplete();
        switch (propName) {
            case Sasl.BOUND_SERVER_NAME:
                return boundServerName;
            case Sasl.MAX_BUFFER:
                return Integer.toString(receivingMaxBuffSize);
            case Sasl.QOP:
                return qop;
            case Sasl.STRENGTH:
                if ("3des".equals(cipher)|| "rc4".equals(cipher)) {
                    return "high";
                } else if ("des".equals(cipher)|| "rc4-56".equals(cipher)) {
                    return "medium";
                } else {
                    return "low";
                }
            default:
                return null;
        }
    }

    /* (non-Javadoc)
     * @see org.wildfly.sasl.util.AbstractSaslParticipant#init()
     */
    @Override
    public void init() {
        setNegotiationState(STEP_ONE);
    }

    @Override
    public byte[] evaluateResponse(byte[] response) throws SaslException {
        return evaluateMessage(response);
    }

    @Override
    protected byte[] evaluateMessage(int state, final byte[] message) throws SaslException {
        switch (state) {
            case STEP_ONE:
                if (message != null && message.length != 0) {
                    throw saslDigest.mechInitialChallengeMustBeEmpty().toSaslException();
                }
                setNegotiationState(STEP_THREE);
                return generateChallenge();
            case STEP_THREE:
                if (message == null || message.length == 0) {
                    throw saslDigest.mechClientRefusesToInitiateAuthentication().toSaslException();
                }

                // parse digest response
                HashMap parsedDigestResponse;
                try {
                    parsedDigestResponse = parseResponse(message, charset, false, saslDigest);
                } catch (AuthenticationMechanismException e) {
                    throw e.toSaslException();
                }
                noteDigestResponseData(parsedDigestResponse);

                // validate
                byte[] response = validateDigestResponse(parsedDigestResponse);

                negotiationComplete();
                return response;
        }
        throw Assert.impossibleSwitchCase(state);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy