/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014 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.password.impl;

import static org.wildfly.security.password.interfaces.BCryptPassword.ALGORITHM_BCRYPT;
import static org.wildfly.security.password.interfaces.BCryptPassword.BCRYPT_SALT_SIZE;
import static org.wildfly.security.password.interfaces.BCryptPassword.DEFAULT_ITERATION_COUNT;

import java.security.Provider;
import java.security.Security;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.wildfly.security.password.WildFlyElytronPasswordProvider;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.interfaces.BCryptPassword;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.password.spec.EncryptablePasswordSpec;
import org.wildfly.security.password.spec.IteratedSaltedPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.IteratedSaltedHashPasswordSpec;
import org.wildfly.security.password.util.ModularCrypt;

/**
 * <p>
 * Tests for the bcrypt password implementation. The expected results in the tests were generated using the
 * <a href="http://www.mindrot.org/projects/jBCrypt/">jBCrypt project</a>.
 * </p>
 *
 * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
 */
public class BCryptPasswordTest {

    private static final Provider provider = WildFlyElytronPasswordProvider.getInstance();

    @BeforeClass
    public static void setup() {
        Security.addProvider(provider);
    }

    @AfterClass
    public static void removeProvider() {
        Security.removeProvider(provider.getName());
    }

    @Test
    public void testGetKeySpecFromString() throws Exception {
        String cryptString = "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG";

        // get the spec by parsing the crypt string.
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        BCryptPassword password = (BCryptPassword) factory.translate(ModularCrypt.decode(cryptString));
        Assert.assertEquals(12, password.getIterationCount());
        Assert.assertEquals(BCryptPassword.BCRYPT_SALT_SIZE, password.getSalt().length);

        // use the spec to build a new crypt string and compare it to the original one.
        Assert.assertEquals(cryptString, ModularCrypt.encodeAsString(password));
    }

    @Test
    public void testHashEmptyString() throws Exception {
        String cryptString = "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye";
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        BCryptPassword password = (BCryptPassword) factory.translate(ModularCrypt.decode(cryptString));

        // use the obtained spec to build a BCryptPasswordImpl, then verify the hash using the correct password.
        Assert.assertTrue(factory.verify(password, "".toCharArray()));

        // check if an incorrect password gets rejected.
        Assert.assertFalse(factory.verify(password, "wrongpassword".toCharArray()));

        // now use the EncryptablePasswordSpec to build a new password and check if the hashed bytes matches those that
        // were parsed and stored in the spec.
        password = (BCryptPassword) factory.generatePassword(new EncryptablePasswordSpec("".toCharArray(),
                new IteratedSaltedPasswordAlgorithmSpec(password.getIterationCount(), password.getSalt())));
        Assert.assertArrayEquals(password.getHash(), password.getHash());

        // use the new password to obtain a spec and then check if the spec yields the same crypt string.
        Assert.assertEquals(cryptString, ModularCrypt.encodeAsString(password));
    }

    @Test
    public void testHashSimpleString() throws Exception {
        String cryptString = "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq";
        char[] correctPassword = "abcdefghijklmnopqrstuvwxyz".toCharArray();
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        BCryptPassword password = (BCryptPassword) factory.translate(ModularCrypt.decode(cryptString));

        password = (BCryptPassword) factory.translate(password);

        // use the obtained spec to build a BCryptPasswordImpl, then verify the hash using the correct password.
        Assert.assertTrue(factory.verify(password, correctPassword));

        // check if an incorrect password gets rejected.
        Assert.assertFalse(factory.verify(password, "wrongpassword".toCharArray()));

        // now use the EncryptablePasswordSpec to build a new password and check if the hashed bytes matches those that
        // were parsed and stored in the spec.
        password = (BCryptPassword) factory.generatePassword(new EncryptablePasswordSpec(correctPassword,
                new IteratedSaltedPasswordAlgorithmSpec(password.getIterationCount(), password.getSalt())));
        Assert.assertArrayEquals(password.getHash(), password.getHash());

        // use the new password to obtain a spec and then check if the spec yields the same crypt string.
        Assert.assertEquals(cryptString, ModularCrypt.encodeAsString(password));
    }

    @Test
    public void testHashComplexString() throws Exception {
        String cryptString = "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC";
        char[] correctPassword = "~!@#$%^&*()      ~!@#$%^&*()PNBFRD".toCharArray();
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        BCryptPassword password = (BCryptPassword) factory.translate(ModularCrypt.decode(cryptString));

        // use the obtained spec to build a BCryptPasswordImpl, then verify the hash using the correct password.
        Assert.assertTrue(factory.verify(password, correctPassword));

        // check if an incorrect password gets rejected.
        Assert.assertFalse(factory.verify(password, "wrongpassword".toCharArray()));

        // now use the EncryptablePasswordSpec to build a new password and check if the hashed bytes matches those that
        // were parsed and stored in the spec.
        password = (BCryptPassword) factory.generatePassword(new EncryptablePasswordSpec(correctPassword,
                new IteratedSaltedPasswordAlgorithmSpec(password.getIterationCount(), password.getSalt())));
        Assert.assertArrayEquals(password.getHash(), password.getHash());

        // use the new password to obtain a spec and then check if the spec yields the same crypt string.
        Assert.assertEquals(cryptString, ModularCrypt.encodeAsString(password));
    }

    /**
     * <p>
     * Test the usage of a {@link org.wildfly.security.password.spec.ClearPasswordSpec} to hash a password. A random
     * salt should be generated and the default iteration count should be used.
     * </p>
     *
     * @throws Exception if an error occurs while running the test.
     */
    @Test
    public void testHashClearPassword() throws Exception {
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        ClearPasswordSpec spec = new ClearPasswordSpec("password".toCharArray());
        BCryptPassword password = (BCryptPassword) factory.generatePassword(spec);

        // check if a salt has been generated and if the default iteration count is being used.
        IteratedSaltedHashPasswordSpec bcryptSpec = factory.getKeySpec(password, IteratedSaltedHashPasswordSpec.class);
        Assert.assertNotNull(bcryptSpec.getSalt());
        Assert.assertEquals(BCRYPT_SALT_SIZE, bcryptSpec.getSalt().length);
        Assert.assertEquals(DEFAULT_ITERATION_COUNT, bcryptSpec.getIterationCount());

        // check if the correct password is verified while an incorrect password is rejected.
        Assert.assertTrue(factory.verify(password, "password".toCharArray()));
        Assert.assertFalse(factory.verify(password, "wrongpassword".toCharArray()));
    }

    /**
     * <p>
     * For this test the crypt string was generated using the Python PassLib bcrypt implementation.
     * </p>
     *
     * @throws Exception if an error occurs while running the test.
     */
    @Test
    public void testHashAgainstPassLib() throws Exception {
        String cryptString = "$2a$12$NT0I31Sa7ihGEWpka9ASYeEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy";
        char[] correctPassword = "password".toCharArray();
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);
        BCryptPassword password = (BCryptPassword) factory.translate(ModularCrypt.decode(cryptString));

        // use the obtained spec to build a BCryptPasswordImpl, then verify the hash using the correct password.
        Assert.assertTrue(factory.verify(password, correctPassword));

        // check if an incorrect password gets rejected.
        Assert.assertFalse(factory.verify(password, "wrongpassword".toCharArray()));

        // now use the EncryptablePasswordSpec to build a new password and check if the hashed bytes matches those that
        // were parsed and stored in the spec.
        password = (BCryptPassword) factory.generatePassword(new EncryptablePasswordSpec(correctPassword,
                new IteratedSaltedPasswordAlgorithmSpec(password.getIterationCount(), password.getSalt())));
        Assert.assertArrayEquals(password.getHash(), password.getHash());

        // use the new password to obtain a spec and then check if the spec yields the same crypt string.
        Assert.assertEquals(cryptString, ModularCrypt.encodeAsString(password));
    }

    @Test
    public void testLongKeys() throws Exception {
        byte[] salt = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                       0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10};
        PasswordFactory factory = PasswordFactory.getInstance(ALGORITHM_BCRYPT);

        // hash a password that is too long (size > 72 bytes).
        String longKey = "01234567890123456789012345678901234567890123456789012345678901234567890123456789";
        BCryptPassword password = (BCryptPassword) factory.generatePassword(
                new EncryptablePasswordSpec(longKey.toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(6, salt)));

        // another long password that shares the first 72 bytes with the original password should yield the same hash.
        String longKeyAlt = "012345678901234567890123456789012345678901234567890123456789012345678901xxxxxxxxyyyyzzzzzz";
        Assert.assertTrue(factory.verify(password, longKeyAlt.toCharArray()));
    }
}
