/* UtilLib - Copyright (c) 2018 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.security;


import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import net.mapeadores.util.base64.Base64;
import net.mapeadores.util.exceptions.NestedLibraryException;
import net.mapeadores.util.text.StringUtils;


/**
 *
 * @author Vincent Calame
 */
public final class PasswordChecker {

    public final static String CLEAR = "clear";
    public final static String MD5 = "md5";
    public final static String PBKDF2 = "pbkdf2";
    private static final int iterations = 20 * 1000;
    private static final int saltLen = 32;
    private static final int desiredKeyLen = 256;


    private PasswordChecker() {

    }

    public static String getHash(String hashMode, String password) {
        if (password == null || password.length() == 0) {
            throw new IllegalArgumentException("Empty passwords are not supported.");
        }
        try {
            switch (hashMode) {
                case CLEAR:
                    return CLEAR + "@" + password;
                case MD5:
                    return MD5 + "@" + getMd5Hash(password);
                case PBKDF2:
                    return PBKDF2 + "@" + getPbkdf2SaltedHash(password);
                default:
                    throw new IllegalArgumentException("Unknown hashMode: " + hashMode);
            }
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new NestedLibraryException(e);
        }
    }

    private static String getMd5Hash(String password) {
        return StringUtils.toMD5(password);
    }

    private static String getPbkdf2SaltedHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
        return Base64.encodeBase64String(salt) + "$" + pbkdf2Hash(password, salt);
    }

    private static String pbkdf2Hash(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecretKey key = f.generateSecret(new PBEKeySpec(
                password.toCharArray(), salt, iterations, desiredKeyLen)
        );
        return Base64.encodeBase64String(key.getEncoded());
    }

    public static boolean check(String password, String stored) {
        if (password.length() == 0) {
            return false;
        }
        String hashMode;
        String storedHash;
        int idx = stored.indexOf('@');
        if (idx == -1) {
            hashMode = MD5;
            storedHash = stored;
        } else {
            hashMode = stored.substring(0, idx);
            storedHash = stored.substring(idx + 1);
        }
        try {
            switch (hashMode) {
                case CLEAR:
                    return password.equals(storedHash);
                case MD5:
                    return checkMd5(password, storedHash);
                case PBKDF2:
                    return checkPbkdf2(password, storedHash);
                default:
                    return false;
            }
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new NestedLibraryException(e);
        }

    }

    public static String getHashMode(String stored) {
        int idx = stored.indexOf('@');
        if (idx == -1) {
            return MD5;
        } else {
            return stored.substring(0, idx);
        }
    }

    private static boolean checkMd5(String password, String storedHash) {
        String md5Value = StringUtils.toMD5(password);
        return md5Value.equals(storedHash);
    }

    private static boolean checkPbkdf2(String password, String storedHash) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String[] saltAndPass = storedHash.split("\\$");
        if (saltAndPass.length != 2) {
            return false;
        }
        String hashOfInput = pbkdf2Hash(password, Base64.decodeBase64(saltAndPass[0]));
        return hashOfInput.equals(saltAndPass[1]);
    }

}
