/*
 * 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 org.junit.Test;
import org.wildfly.security.password.Password;
import org.wildfly.security.password.interfaces.UnixSHACryptPassword;
import org.wildfly.security.password.spec.EncryptablePasswordSpec;
import org.wildfly.security.password.util.ModularCrypt;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.wildfly.security.password.interfaces.UnixSHACryptPassword.ALGORITHM_CRYPT_SHA_256;
import static org.wildfly.security.password.interfaces.UnixSHACryptPassword.ALGORITHM_CRYPT_SHA_512;

import org.wildfly.security.password.spec.IteratedSaltedPasswordAlgorithmSpec;

/**
 * @author <a href="mailto:jpkroehling.javadoc@redhat.com">Juraci Paixão Kröhling</a>
 */
@SuppressWarnings("SpellCheckingInspection")
public class UnixSHACryptPasswordUtilTest {

    @Test
    public void shouldParseSpecWithoutRounds() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals("Didn't parse the number of rounds correctly", 5_000, ((UnixSHACryptPassword) ModularCrypt.decode("$6$toolongsaltstrin$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")).getIterationCount());
    }

    @Test
    public void shouldParseSpecWithRounds() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals("Didn't parse the number of rounds correctly", 10_000, ((UnixSHACryptPassword) ModularCrypt.decode("$6$rounds=10000$saltstring$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")).getIterationCount());
    }

    @Test
    public void shouldTruncateSaltAt16Chars() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals("Didn't parse the number of rounds correctly", 5_000, ((UnixSHACryptPassword) ModularCrypt.decode("$6$rounds=5000$toolongsaltstrin$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")).getIterationCount());
    }

    @Test
    public void shouldIncreaseIterationCountIfLowerThan1000() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals("Didn't increase the number of rounds", 1_000, ((UnixSHACryptPassword) ModularCrypt.decode("$6$rounds=10$roundstoobig$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")).getIterationCount());
    }

    @Test
    public void shouldDecreaseIterationCountIfBiggerThan999999999() throws NoSuchAlgorithmException, InvalidKeySpecException {
        // this test is being kept as a way to mark that this behavior is intended, but it's not tested with the
        // usual tests because it would run the hashing with 999,999,999 iterations
        assertEquals("Didn't decrease the number of rounds", 999_999_999, ((UnixSHACryptPassword) ModularCrypt.decode("$6$rounds=1000000000$roundstoobig$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")).getIterationCount());
    }

    @Test
    public void shouldVerifyOnMatchingHashes() throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {
        String cryptString = "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA";
        final PasswordFactorySpiImpl factorySpi = new PasswordFactorySpiImpl();
        final Password parsed = ModularCrypt.decode(cryptString);
        assertEquals(cryptString, ModularCrypt.encodeAsString(parsed));
        UnixSHACryptPassword password = (UnixSHACryptPassword) parsed;
        final String algorithm = password.getAlgorithm();
        UnixSHACryptPassword comparePassword = (UnixSHACryptPassword) factorySpi.engineGeneratePassword(algorithm, new EncryptablePasswordSpec("Hello world!".toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(10000, password.getSalt())));
        assertEquals(cryptString, ModularCrypt.encodeAsString(comparePassword));
        assertEquals(password.getIterationCount(), comparePassword.getIterationCount());
        assertArrayEquals(password.getSalt(), comparePassword.getSalt());
        assertArrayEquals(password.getHash(), comparePassword.getHash());

        // password is in raw form, need to translate first before verifying
        password = (UnixSHACryptPassword) factorySpi.engineTranslatePassword(algorithm, password);

        assertTrue(factorySpi.engineVerify(algorithm, password, "Hello world!".toCharArray()));
    }

    @Test
    public void shouldNotVerifyOnNonMatchingHashes() {

    }

    private String generate(String alg, String salt, String passwd, int iterationCount) throws InvalidKeySpecException {
        final PasswordFactorySpiImpl spi = new PasswordFactorySpiImpl();
        final Password password = spi.engineGeneratePassword(alg, new EncryptablePasswordSpec(passwd.toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt.getBytes(UTF_8))));
        return ModularCrypt.encodeAsString(password);
    }

    @Test
    public void shouldPassAllCasesFromSpecForSha256() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals(
                "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5",
                generate(ALGORITHM_CRYPT_SHA_256, "saltstring", "Hello world!", 5_000)
        );

        assertEquals(
                "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA",
                generate(ALGORITHM_CRYPT_SHA_256, "saltstringsaltstring", "Hello world!", 10_000)
        );

        assertEquals(
                "$5$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5",
                generate(ALGORITHM_CRYPT_SHA_256, "toolongsaltstring", "This is just a test", 5_000)
        );

        assertEquals(
                "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12oP84Bnq1",
                generate(ALGORITHM_CRYPT_SHA_256, "anotherlongsaltstring", "a very much longer text to encrypt.  This one even stretches over morethan one line.", 1_400)
        );

        assertEquals(
                "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/",
                generate(ALGORITHM_CRYPT_SHA_256, "short", "we have a short salt string but not a short password", 77_777)
        );

        assertEquals(
                "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/cZKmF/wJvD",
                generate(ALGORITHM_CRYPT_SHA_256, "asaltof16chars..", "a short string", 123_456)
        );

        assertEquals(
                "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC",
                generate(ALGORITHM_CRYPT_SHA_256, "roundstoolow", "the minimum number is still observed", 10)
        );
    }

    @Test
    public void shouldPassAllCasesFromSpecForSha512() throws NoSuchAlgorithmException, InvalidKeySpecException {
        assertEquals(
                "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1",
                generate(ALGORITHM_CRYPT_SHA_512, "saltstring", "Hello world!", 5_000)
        );

        assertEquals(
                "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.",
                generate(ALGORITHM_CRYPT_SHA_512, "saltstringsaltstring", "Hello world!", 10_000)
        );

        assertEquals(
                "$6$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0",
                generate(ALGORITHM_CRYPT_SHA_512, "toolongsaltstring", "This is just a test", 5_000)
        );

        assertEquals(
                "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1",
                generate(ALGORITHM_CRYPT_SHA_512, "anotherlongsaltstring", "a very much longer text to encrypt.  This one even stretches over morethan one line.", 1_400)
        );

        assertEquals(
                "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0",
                generate(ALGORITHM_CRYPT_SHA_512, "short", "we have a short salt string but not a short password", 77_777)
        );

        assertEquals(
                "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1",
                generate(ALGORITHM_CRYPT_SHA_512, "asaltof16chars..", "a short string", 123_456)
        );

        assertEquals(
                "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.",
                generate(ALGORITHM_CRYPT_SHA_512, "roundstoolow", "the minimum number is still observed", 10)
        );
    }

}
