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" );
    }
}
