JiveSharedSecretSaslServer.java 5.77 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
package org.jivesoftware.openfire.sasl;

import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;

import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;

/**
 * Implementation of a proprietary Jive Software SASL mechanism that is based on a shared secret. Successful
 * authentication will result in an anonymous authorization.
 *
 * @author Guus der Kinderen, guus@goodbytes.nl
 */
public class JiveSharedSecretSaslServer implements SaslServer
{
    public static final String NAME = "JIVE-SHAREDSECRET";

    private boolean complete = false;

    @Override
    public String getMechanismName()
    {
        return NAME;
    }

    @Override
    public byte[] evaluateResponse( byte[] response ) throws SaslException
    {
        if ( isComplete() )
        {
            throw new IllegalStateException( "Authentication exchange already completed." );
        }

        if ( response == null || response.length == 0 )
        {
            // No info was provided so send a challenge to get it.
            return new byte[ 0 ];
        }

        complete = true;

        // Parse data and obtain username & password.
        final StringTokenizer tokens = new StringTokenizer( new String( response, StandardCharsets.UTF_8 ), "\0" );
        tokens.nextToken();
        final String secretDigest = tokens.nextToken();

        if ( authenticateSharedSecret( secretDigest ) )
        {
            return null; // Success!
        }
        else
        {
            // Otherwise, authentication failed.
            throw new SaslException( "Authentication failed" );
        }
    }

    @Override
    public boolean isComplete()
    {
        return complete;
    }

    @Override
    public String getAuthorizationID()
    {
        if ( !isComplete() )
        {
            throw new IllegalStateException( "Authentication exchange not completed." );
        }

        return null; // Anonymous!
    }

    @Override
    public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
    {
        if ( !isComplete() )
        {
            throw new IllegalStateException( "Authentication exchange not completed." );
        }

        throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
    }

    @Override
    public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
    {
        if ( !isComplete() )
        {
            throw new IllegalStateException( "Authentication exchange not completed." );
        }

        throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
    }

    @Override
    public Object getNegotiatedProperty( String propName )
    {
        if ( !isComplete() )
        {
            throw new IllegalStateException( "Authentication exchange not completed." );
        }

        if ( propName.equals( Sasl.QOP ) )
        {
            return "auth";
        }
        else
        {
            return null;
        }
    }

    @Override
    public void dispose() throws SaslException
    {
        complete = false;
    }

    /**
     * Returns true if the supplied digest matches the shared secret value. The digest must be an MD5 hash of the secret
     * key, encoded as hex. This value is supplied by clients attempting shared secret authentication.
     *
     * @param digest the MD5 hash of the secret key, encoded as hex.
     * @return true if authentication succeeds.
     */
    public static boolean authenticateSharedSecret( String digest )
    {
        if ( !isSharedSecretAllowed() )
        {
            return false;
        }

        return StringUtils.hash( getSharedSecret() ).equals( digest );
    }

    /**
     * Returns true if shared secret authentication is enabled. Shared secret authentication creates an anonymous
     * session, but requires that the authenticating entity know a shared secret key. The client sends a digest of the
     * secret key, which is compared against a digest of the local shared key.
     *
     * @return true if shared secret authentication is enabled.
     */
    public static boolean isSharedSecretAllowed()
    {
        return JiveGlobals.getBooleanProperty( "xmpp.auth.sharedSecretEnabled" );
    }

    /**
     * Returns the shared secret value, or <tt>null</tt> if shared secret authentication is disabled. If this is the
     * first time the shared secret value has been requested (and  shared secret auth is enabled), the key will be
     * randomly generated and stored in the property <tt>xmpp.auth.sharedSecret</tt>.
     *
     * @return the shared secret value.
     */
    public static String getSharedSecret()
    {
        if ( !isSharedSecretAllowed() )
        {
            return null;
        }

        String sharedSecret = JiveGlobals.getProperty( "xmpp.auth.sharedSecret" );
        if ( sharedSecret == null )
        {
            sharedSecret = StringUtils.randomString( 8 );
            JiveGlobals.setProperty( "xmpp.auth.sharedSecret", sharedSecret );
        }
        return sharedSecret;
    }

    /**
     * Sets whether shared secret authentication is enabled. Shared secret authentication creates an anonymous session,
     * but requires that the authenticating entity know a shared secret key. The client sends a digest of the secret
     * key, which is compared against a digest of the local shared key.
     *
     * @param sharedSecretAllowed true if shared secret authentication should be enabled.
     */
    public static void setSharedSecretAllowed( boolean sharedSecretAllowed )
    {
        JiveGlobals.setProperty( "xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false" );
    }
}