/**
 * Copyright 2005-2013 Restlet S.A.S.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL
 * 1.0 (the "Licenses"). You can select the license that you prefer but you may
 * not use this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the Apache 2.0 license at
 * http://www.opensource.org/licenses/apache-2.0
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.restlet.com/products/restlet-framework
 * 
 * Restlet is a registered trademark of Restlet S.A.S.
 */

package org.restlet.ext.crypto;

import java.util.logging.Level;

import org.restlet.Context;
import org.restlet.data.Digest;
import org.restlet.security.LocalVerifier;
import org.restlet.security.SecretVerifier;

/**
 * Wrapper verifier that can verify digested secrets. If the provided secret is
 * a digest, then the local secret must either be a digest of the same algorithm
 * or the wrapped verifier must be a {@link LocalVerifier} returning secrets in
 * clear.<br>
 * <br>
 * If the provided secret is a regular secret, then the local secret can be in
 * any digest algorithm or a regular secret.
 * 
 * @see Digest
 * @see DigestAuthenticator
 * @author Jerome Louvel
 */
public class DigestVerifier<T extends SecretVerifier> extends SecretVerifier {

    /** The digest algorithm of provided secrets. */
    private String algorithm;

    /** The digest algorithm of secrets returned by the wrapped verifier. */
    private String wrappedAlgorithm;

    /** The wrapped secret verifier. */
    private T wrappedVerifier;

    /**
     * Constructor.
     * 
     * @param algorithm
     *            The digest algorithm of provided secrets.
     * @param wrappedVerifier
     *            The wrapped secret verifier.
     * @param wrappedAlgorithm
     *            The digest algorithm of secrets provided by the wrapped
     *            verifier.
     * @see Digest
     */
    public DigestVerifier(String algorithm, T wrappedVerifier,
            String wrappedAlgorithm) {
        this.algorithm = algorithm;
        this.wrappedAlgorithm = wrappedAlgorithm;
        this.wrappedVerifier = wrappedVerifier;
    }

    /**
     * Computes the digest of a secret according to a specified algorithm. By
     * default, MD5 hashes (represented as a sequence of 32 hexadecimal digits)
     * and SHA-1 hashes are supported. For additional algorithm, override this
     * method.
     * 
     * @param identifier
     *            The user identifier.
     * @param secret
     *            The regular secret to digest.
     * @param algorithm
     *            The digest algorithm to use.
     * @return The digested secret.
     * @see Digest
     */
    protected char[] digest(String identifier, char[] secret, String algorithm) {
        return DigestUtils.digest(secret, algorithm);
    }

    /**
     * Returns the digest algorithm of provided secrets. Provided secrets are
     * the ones sent by clients when attempting to authenticate.
     * 
     * @return The digest algorithm of input secrets.
     */
    public String getAlgorithm() {
        return algorithm;
    }

    /**
     * Returns the digest algorithm of secrets returned by the wrapped verifier.
     * The secrets from the wrapped verifier are the ones used by the verifier
     * to compare those sent by clients when attempting to authenticate.
     * 
     * @return The digest algorithm of secrets returned by the wrapped verifier.
     */
    public String getWrappedAlgorithm() {
        return wrappedAlgorithm;
    }

    /**
     * Returns the wrapped secret associated to a given identifier. This method
     * can only be called if the wrapped verifier is a {@link LocalVerifier}.
     * 
     * @param identifier
     *            The identifier to lookup.
     * @return The secret associated to the identifier or null.
     */
    public char[] getWrappedSecret(String identifier) {
        char[] result = null;

        if (getWrappedVerifier() instanceof LocalVerifier) {
            LocalVerifier localVerifier = (LocalVerifier) getWrappedVerifier();
            result = localVerifier.getLocalSecret(identifier);
        } else {
            Context.getCurrentLogger()
                    .log(Level.WARNING,
                            "The wrapped verifier must be a LocalVerifier to allow digesting of wrapped secrets.");
        }

        return result;
    }

    /**
     * Returns the digest of the wrapped secret associated to a given
     * identifier. If the wrapped algorithm is null it returns the digest of the
     * wrapped secret, otherwise the algorithms must be identical. This method
     * can only be called if the wrapped verifier is a {@link LocalVerifier}.
     * 
     * @param identifier
     *            The identifier to lookup.
     * @return The secret associated to the identifier or null.
     */
    public char[] getWrappedSecretDigest(String identifier) {
        char[] result = null;

        if (getWrappedAlgorithm() == null) {
            result = digest(identifier, getWrappedSecret(identifier),
                    getAlgorithm());
        } else if (getAlgorithm().equals(getWrappedAlgorithm())) {
            result = getWrappedSecret(identifier);
        } else {
            Context.getCurrentLogger().log(Level.WARNING,
                    "The digest algorithms can't be different.");
        }

        return result;
    }

    /**
     * Returns the wrapped secret verifier.
     * 
     * @return The wrapped secret verifier.
     */
    public T getWrappedVerifier() {
        return wrappedVerifier;
    }

    /**
     * Sets the digest algorithm of provided secrets. Provided secrets are the
     * ones sent by clients when attempting to authenticate.
     * 
     * @param algorithm
     *            The digest algorithm of secrets provided by the user.
     * @see Digest
     */
    public void setAlgorithm(String algorithm) {
        this.algorithm = algorithm;
    }

    /**
     * Sets the digest algorithm of secrets returned by the wrapped verifier.
     * The secrets from the wrapped verifier are the ones used by the verifier
     * to compare those sent by clients when attempting to authenticate.
     * 
     * @param wrappedAlgorithm
     *            The digest algorithm of secrets returned by the wrapped
     *            verifier.
     * @see Digest
     */
    public void setWrappedAlgorithm(String wrappedAlgorithm) {
        this.wrappedAlgorithm = wrappedAlgorithm;
    }

    /**
     * Sets the wrapped secret verifier.
     * 
     * @param wrappedVerifier
     *            The wrapped secret verifier.
     */
    public void setWrappedVerifier(T wrappedVerifier) {
        this.wrappedVerifier = wrappedVerifier;
    }

    @Override
    public int verify(String identifier, char[] secret) {
        int result = RESULT_INVALID;
        char[] secretDigest = secret;

        if (getAlgorithm() == null) {
            if (getWrappedAlgorithm() != null) {
                secretDigest = digest(identifier, secret, getWrappedAlgorithm());
            } else {
                // Both secrets should be in clear
            }

            result = getWrappedVerifier().verify(identifier, secretDigest);
        } else {
            if (getWrappedAlgorithm() == null) {
                result = compare(secretDigest,
                        getWrappedSecretDigest(identifier)) ? RESULT_VALID
                        : RESULT_INVALID;
            } else if (getAlgorithm().equals(getWrappedAlgorithm())) {
                result = getWrappedVerifier().verify(identifier, secretDigest);
            } else {
                result = RESULT_UNSUPPORTED;
                Context.getCurrentLogger().log(Level.WARNING,
                        "The input and output algorithms can't be different.");
            }
        }

        return result;
    }
}
