/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015 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.mechanism.scram;

import java.security.Provider;
import java.security.SecureRandom;
import java.util.function.Supplier;

import javax.security.auth.callback.CallbackHandler;

import org.wildfly.security.auth.callback.ChannelBindingCallback;
import org.wildfly.security.mechanism.AuthenticationMechanismException;
import org.wildfly.security.password.interfaces.ScramDigestPassword;
import org.wildfly.security.sasl.WildFlySasl;

/**
 * Implementation of the SCRAM authentication mechanism.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public final class ScramMechanism {
    // Hash size; may be less than the output size of the MD/MAC
    private final int hashSize;
    private final String messageDigestName;
    private final String hmacName;
    private final boolean plus;
    private final String passwordAlgorithm;
    private final String toString;

    /**
     * Constructs a new {@code ScramMechanism}.
     *
     * @param hashSize the size of the hash of the SCRAM mechanism.
     * @param messageDigestName the name of the message digest algorithm.
     * @param hmacName the name of the HMAC algorithm.
     * @param plus {@code true} to use the PLUS channel binding, {@code false} otherwise.
     * @param passwordAlgorithm the name of the password algorithm in {@link ScramDigestPassword}.
     */
    private ScramMechanism(final int hashSize, final String messageDigestName, final String hmacName, final boolean plus, final String passwordAlgorithm) {
        this.hashSize = hashSize;
        this.messageDigestName = messageDigestName;
        this.hmacName = hmacName;
        this.plus = plus;
        this.passwordAlgorithm = passwordAlgorithm;
        StringBuilder b = new StringBuilder();
        b.append("SCRAM ").append(messageDigestName).append(' ');
        if (plus) b.append("(PLUS channel binding) ");
        b.append(hashSize * 8).append(" bits");
        toString = b.toString();
    }

    public static final ScramMechanism SCRAM_SHA_1 = new ScramMechanism(20, "SHA-1", "HmacSHA1", false, ScramDigestPassword.ALGORITHM_SCRAM_SHA_1);
    public static final ScramMechanism SCRAM_SHA_1_PLUS = new ScramMechanism(20, "SHA-1", "HmacSHA1", true, ScramDigestPassword.ALGORITHM_SCRAM_SHA_1);

    public static final ScramMechanism SCRAM_SHA_256 = new ScramMechanism(32, "SHA-256", "HmacSHA256", false, ScramDigestPassword.ALGORITHM_SCRAM_SHA_256);
    public static final ScramMechanism SCRAM_SHA_256_PLUS = new ScramMechanism(32, "SHA-256", "HmacSHA256", true, ScramDigestPassword.ALGORITHM_SCRAM_SHA_256);

    public static final ScramMechanism SCRAM_SHA_384 = new ScramMechanism(48, "SHA-384", "HmacSHA384", false, ScramDigestPassword.ALGORITHM_SCRAM_SHA_384);
    public static final ScramMechanism SCRAM_SHA_384_PLUS = new ScramMechanism(32, "SHA-384", "HmacSHA384", true, ScramDigestPassword.ALGORITHM_SCRAM_SHA_384);

    public static final ScramMechanism SCRAM_SHA_512 = new ScramMechanism(64, "SHA-512", "HmacSHA512", false, ScramDigestPassword.ALGORITHM_SCRAM_SHA_512);
    public static final ScramMechanism SCRAM_SHA_512_PLUS = new ScramMechanism(64, "SHA-512", "HmacSHA512", true, ScramDigestPassword.ALGORITHM_SCRAM_SHA_512);

    /**
     * Create a SCRAM client for this mechanism.
     *
     * @param authorizationId the authorization ID ({@code null} if none is given)
     * @param callbackHandler the callback handler (may not be {@code null})
     * @param secureRandom an optional secure random implementation to use (may be {@code null})
     * @param bindingCallback the optional channel binding callback result (may be {@code null})
     * @param minimumIterationCount the minimum iteration count to allow
     * @param maximumIterationCount the maximum iteration count to allow
     * @param providers the security providers.
     * @return the SCRAM client, or {@code null} if the client cannot be created from this mechanism variant
     * @throws AuthenticationMechanismException if the mechanism fails for some reason
     * @see WildFlySasl#SCRAM_MIN_ITERATION_COUNT
     * @see WildFlySasl#SCRAM_MAX_ITERATION_COUNT
     */
    public ScramClient createClient(final String authorizationId, final CallbackHandler callbackHandler, final SecureRandom secureRandom, final ChannelBindingCallback bindingCallback, final int minimumIterationCount, final int maximumIterationCount, final Supplier<Provider[]> providers) throws AuthenticationMechanismException {
        final byte[] bindingData;
        final String bindingType;
        if (bindingCallback != null) {
            bindingData = bindingCallback.getBindingData();
            bindingType = bindingCallback.getBindingType();
        } else {
            if (plus) return null;
            bindingData = null;
            bindingType = null;
        }
        return new ScramClient(this, authorizationId, callbackHandler, secureRandom, bindingData, bindingType, minimumIterationCount, maximumIterationCount, providers);
    }

    /**
     * Create a SCRAM server for this mechanism.
     *
     * @param callbackHandler the callback handler (may not be {@code null}).
     * @param random an optional secure random implementation to use (may be {@code null}).
     * @param bindingCallback the optional channel binding callback result (may be {@code null}).
     * @param minimumIterationCount the minimum iteration count to allow.
     * @param maximumIterationCount the maximum iteration count to allow.
     * @param providers the security providers.
     * @return the SCRAM server, or {@code null} if the server cannot be created from this mechanism variant.
     * @throws AuthenticationMechanismException if the mechanism fails for some reason.
     */
    public ScramServer createServer(final CallbackHandler callbackHandler, final SecureRandom random, final ChannelBindingCallback bindingCallback, final int minimumIterationCount, final int maximumIterationCount, final Supplier<Provider[]> providers) throws AuthenticationMechanismException {
        final byte[] bindingData;
        final String bindingType;
        if (bindingCallback != null) {
            bindingData = bindingCallback.getBindingData();
            bindingType = bindingCallback.getBindingType();
        } else {
            if (plus) return null;
            bindingData = null;
            bindingType = null;
        }
        return new ScramServer(this, callbackHandler, random, bindingData, bindingType, minimumIterationCount, maximumIterationCount, providers);
    }

    /**
     * Returns the size of the hash of the SCRAM mechanism.
     *
     * @return the size of the hash of the SCRAM mechanism.
     */
    public int getHashSize() {
        return hashSize;
    }

    /**
     * Returns the name of the message digest algorithm.
     *
     * @return the name of the message digest algorithm.
     */
    public String getMessageDigestName() {
        return messageDigestName;
    }

    /**
     * Returns the name of the HMAC algorithm.
     *
     * @return the name of the HMAC algorithm.
     */
    public String getHmacName() {
        return hmacName;
    }

    /**
     * Returns whether the SCRAM mechanism uses the PLUS channel binding.
     *
     * @return {@code true} to use the PLUS channel binding, {@code false} otherwise.
     */
    public boolean isPlus() {
        return plus;
    }

    /**
     * Returns the name of the password algorithm from {@code ScramDigestPassword}.
     *
     * @return the name of the password algorithm.
     */
    public String getPasswordAlgorithm() {
        return passwordAlgorithm;
    }

    /**
     * Returns a String representation of the SCRAM mechanism.
     * Contains the Digest name, PLUS channel binding and hash size.
     *
     * @return a String representation of the SCRAM mechanism.
     */
    public String toString() {
        return toString;
    }
}
