/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * 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.auth.realm.jdbc;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.wildfly.security.auth.realm.jdbc.DataSourceRule.ELYTRON_PASSWORD_PROVIDERS;
import static org.wildfly.security.password.interfaces.BCryptPassword.BCRYPT_SALT_SIZE;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ThreadLocalRandom;

import org.junit.ClassRule;
import org.junit.Test;
import org.wildfly.common.iteration.ByteIterator;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.realm.jdbc.mapper.PasswordKeyMapper;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.credential.PasswordCredential;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.interfaces.BCryptPassword;
import org.wildfly.security.password.interfaces.ClearPassword;
import org.wildfly.security.password.interfaces.SaltedSimpleDigestPassword;
import org.wildfly.security.password.interfaces.ScramDigestPassword;
import org.wildfly.security.password.interfaces.SimpleDigestPassword;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.password.spec.Encoding;
import org.wildfly.security.password.spec.EncryptablePasswordSpec;
import org.wildfly.security.password.spec.IteratedSaltedHashPasswordSpec;
import org.wildfly.security.password.spec.IteratedSaltedPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.SaltedPasswordAlgorithmSpec;
import org.wildfly.security.password.util.ModularCrypt;

/**
 * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
 */
public class PasswordSupportTest {

    @ClassRule
    public static final DataSourceRule dataSourceRule = new DataSourceRule();

    @Test
    public void testVerifyAndObtainClearPasswordCredential() throws Exception {
        String userName = "john";
        String userPassword = "abcd1234";

        createClearPasswordTable(userName, userPassword);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(ClearPassword.ALGORITHM_CLEAR)
            .setHashColumn(1)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .principalQuery("SELECT password FROM user_clear_password WHERE name = ?")
                    .withMapper(passwordKeyMapper)
                    .from(dataSourceRule.getDataSource())
                .build();

        assertTrue(securityRealm.getCredentialAcquireSupport(PasswordCredential.class, ClearPassword.ALGORITHM_CLEAR, null).mayBeSupported());

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertTrue(realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, ClearPassword.ALGORITHM_CLEAR, null).isDefinitelySupported());

        PasswordFactory passwordFactory = PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR, ELYTRON_PASSWORD_PROVIDERS);
        ClearPassword password = (ClearPassword) passwordFactory.generatePassword(new ClearPasswordSpec(userPassword.toCharArray()));

        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));

        PasswordGuessEvidence invalidPassword = new PasswordGuessEvidence("badpasswd".toCharArray());

        assertFalse(realmIdentity.verifyEvidence(invalidPassword));

        ClearPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, ClearPassword.ALGORITHM_CLEAR).getPassword(ClearPassword.class);

        assertNotNull(storedPassword);
        assertArrayEquals(password.getPassword(), storedPassword.getPassword());
    }

    @Test
    public void testVerifyAndObtainBCryptPasswordCredentialUsingModularCrypt() throws Exception {
        String userName = "john";
        String userPassword = "bcrypt_abcd1234";

        String cryptString = createBcryptModularCryptPasswordTable(userName, userPassword);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(BCryptPassword.ALGORITHM_BCRYPT)
            .setHashColumn(1)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT password FROM user_bcrypt_modular_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));
        assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("invalid".toCharArray())));

        BCryptPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT).getPassword(BCryptPassword.class);

        assertNotNull(storedPassword);

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

    @Test
    public void testVerifyAndObtainBCryptPasswordCredential() throws Exception {
        String userName = "john";
        String userPassword = "bcrypt_abcd1234";
        byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
        int iterationCount = 10;

        createBcryptPasswordTable(userName, userPassword, salt, iterationCount);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(BCryptPassword.ALGORITHM_BCRYPT)
            .setHashColumn(1)
            .setSaltColumn(2)
            .setIterationCountColumn(3)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT password, salt, iterationCount FROM user_bcrypt_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));
        assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("invalid".toCharArray())));

        BCryptPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT).getPassword(BCryptPassword.class);

        assertNotNull(storedPassword);
    }

    @Test
    public void testVerifyAndObtainBCryptEncodedPasswordCredential() throws Exception {
        String userName = "john";
        String userPassword = "bcrypt_abcd1234";
        byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
        int iterationCount = 10;

        createBcryptEncodedTable(userName, userPassword, salt, iterationCount);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
                .setDefaultAlgorithm(BCryptPassword.ALGORITHM_BCRYPT)
                .setHashColumn(1).setHashEncoding(Encoding.HEX)
                .setSaltColumn(2).setSaltEncoding(Encoding.BASE64)
                .setIterationCountColumn(3)
                .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT password, salt, iterationCount FROM user_bcrypt_encoded_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));
        assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("invalid".toCharArray())));

        BCryptPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT).getPassword(BCryptPassword.class);

        assertNotNull(storedPassword);
    }

    @Test
    public void testVerifyAndObtainBCryptDifferentCharsetEncodedPasswordCredential() throws Exception {
        String userName = "john";
        String userPassword = "password密码";
        byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
        int iterationCount = 10;

        createBcryptEncodedTable(userName, userPassword, salt, iterationCount, Charset.forName("gb2312"));

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
                .setDefaultAlgorithm(BCryptPassword.ALGORITHM_BCRYPT)
                .setHashColumn(1).setHashEncoding(Encoding.HEX)
                .setSaltColumn(2).setSaltEncoding(Encoding.BASE64)
                .setIterationCountColumn(3)
                .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .setHashCharset(Charset.forName("gb2312"))
                .principalQuery("SELECT password, salt, iterationCount FROM user_bcrypt_encoded_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));
        assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("invalid".toCharArray())));

        BCryptPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, BCryptPassword.ALGORITHM_BCRYPT).getPassword(BCryptPassword.class);

        assertNotNull(storedPassword);
    }

    @Test
    public void testVerifyAndObtainSaltedDigestPasswordCredential() throws Exception {
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_SALT_PASSWORD_DIGEST_SHA_512);
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_SALT_PASSWORD_DIGEST_SHA_1);
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_SALT_PASSWORD_DIGEST_SHA_384);
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_PASSWORD_SALT_DIGEST_SHA_512);
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_PASSWORD_SALT_DIGEST_SHA_1);
        assertVerifyAndObtainSaltedDigestPasswordCredential(SaltedSimpleDigestPassword.ALGORITHM_PASSWORD_SALT_DIGEST_SHA_384);
    }

    public void assertVerifyAndObtainSaltedDigestPasswordCredential(String algorithm) throws Exception {
        String userName = "john";
        String userPassword = "salted_digest_abcd1234";

        SaltedSimpleDigestPassword password = createSaltedDigestPasswordTable(algorithm, userName, userPassword);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(algorithm)
            .setHashColumn(1)
            .setSaltColumn(2)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT digest, salt FROM user_salted_digest_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, algorithm, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, algorithm, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));

        SaltedSimpleDigestPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, algorithm).getPassword(SaltedSimpleDigestPassword.class);

        assertNotNull(storedPassword);
        assertArrayEquals(password.getDigest(), storedPassword.getDigest());
        assertArrayEquals(password.getSalt(), storedPassword.getSalt());
    }

    @Test
    public void testVerifySimpleDigestPasswordCredential() throws Exception {
        assertVerifyAndObtainSimpleDigestPasswordCredential(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_512);
        assertVerifyAndObtainSimpleDigestPasswordCredential(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_MD5);
        assertVerifyAndObtainSimpleDigestPasswordCredential(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_MD2);
    }

    public void assertVerifyAndObtainSimpleDigestPasswordCredential(String algorithm) throws Exception {
        String userName = "john";
        String userPassword = "simple_digest_abcd1234";

        SimpleDigestPassword password = createSimpleDigestPasswordTable(algorithm, userName, userPassword);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(algorithm)
            .setHashColumn(1)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT digest FROM user_simple_digest_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, algorithm, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, algorithm, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));

        SimpleDigestPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, algorithm).getPassword(SimpleDigestPassword.class);

        assertNotNull(storedPassword);
        assertArrayEquals(password.getDigest(), storedPassword.getDigest());
    }

    @Test
    public void testVerifyAndObtainScramDigestPasswordCredential() throws Exception {
        String userName = "john";
        String userPassword = "scram_digest_abcd1234";

        IteratedSaltedHashPasswordSpec passwordSpec = createScramDigestPasswordTable(userName, userPassword);

        PasswordKeyMapper passwordKeyMapper = PasswordKeyMapper.builder()
            .setDefaultAlgorithm(ScramDigestPassword.ALGORITHM_SCRAM_SHA_256)
            .setHashColumn(1)
            .setSaltColumn(2)
            .setIterationCountColumn(3)
            .build();

        JdbcSecurityRealm securityRealm = JdbcSecurityRealm.builder()
                .principalQuery("SELECT digest, salt, iterationCount FROM user_scram_digest_password where name = ?")
                .withMapper(passwordKeyMapper)
                .from(dataSourceRule.getDataSource())
                .setProviders(ELYTRON_PASSWORD_PROVIDERS)
                .build();

        assertEquals(SupportLevel.POSSIBLY_SUPPORTED, securityRealm.getCredentialAcquireSupport(PasswordCredential.class, ScramDigestPassword.ALGORITHM_SCRAM_SHA_256, null));

        RealmIdentity realmIdentity = securityRealm.getRealmIdentity(new NamePrincipal(userName));

        assertEquals(SupportLevel.SUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, ScramDigestPassword.ALGORITHM_SCRAM_SHA_256, null));
        assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence(userPassword.toCharArray())));

        ScramDigestPassword storedPassword = realmIdentity.getCredential(PasswordCredential.class, ScramDigestPassword.ALGORITHM_SCRAM_SHA_256).getPassword(ScramDigestPassword.class);

        assertNotNull(storedPassword);
        assertArrayEquals(passwordSpec.getHash(), storedPassword.getDigest());
        assertArrayEquals(passwordSpec.getSalt(), storedPassword.getSalt());
        assertEquals(passwordSpec.getIterationCount(), storedPassword.getIterationCount());
    }

    private SaltedSimpleDigestPassword createSaltedDigestPasswordTable(String algorithm, String userName, String userPassword) throws Exception {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_salted_digest_password");
            statement.executeUpdate("CREATE TABLE user_salted_digest_password ( id INTEGER IDENTITY, name VARCHAR(100), digest OTHER, salt OTHER)");
        }

        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_salted_digest_password (name, digest, salt) VALUES (?, ?, ?)");
        ) {
            byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
            SaltedPasswordAlgorithmSpec spac = new SaltedPasswordAlgorithmSpec(salt);
            EncryptablePasswordSpec eps = new EncryptablePasswordSpec(userPassword.toCharArray(), spac);
            PasswordFactory passwordFactory = PasswordFactory.getInstance(algorithm, ELYTRON_PASSWORD_PROVIDERS);
            SaltedSimpleDigestPassword tsdp = (SaltedSimpleDigestPassword) passwordFactory.generatePassword(eps);

            preparedStatement.setString(1, userName);
            preparedStatement.setBytes(2, tsdp.getDigest());
            preparedStatement.setBytes(3, tsdp.getSalt());
            preparedStatement.execute();

            return tsdp;
        }
    }

    private SimpleDigestPassword createSimpleDigestPasswordTable(String algorithm, String userName, String userPassword) throws Exception {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_simple_digest_password");
            statement.executeUpdate("CREATE TABLE user_simple_digest_password ( id INTEGER IDENTITY, name VARCHAR(100), digest OTHER)");
        }

        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_simple_digest_password (name, digest) VALUES (?, ?)");
        ) {
            EncryptablePasswordSpec eps = new EncryptablePasswordSpec(userPassword.toCharArray(), null);
            PasswordFactory passwordFactory = PasswordFactory.getInstance(algorithm, ELYTRON_PASSWORD_PROVIDERS);
            SimpleDigestPassword tsdp = (SimpleDigestPassword) passwordFactory.generatePassword(eps);

            preparedStatement.setString(1, userName);
            preparedStatement.setBytes(2, tsdp.getDigest());
            preparedStatement.execute();

            return tsdp;
        }
    }

    private IteratedSaltedHashPasswordSpec createScramDigestPasswordTable(String userName, String userPassword) throws Exception {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_scram_digest_password");
            statement.executeUpdate("CREATE TABLE user_scram_digest_password ( id INTEGER IDENTITY, name VARCHAR(100), digest OTHER, salt OTHER, iterationCount INTEGER)");
        }

        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_scram_digest_password (name, digest, salt, iterationCount) VALUES (?, ?, ?, ?)");
        ) {
            byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
            PasswordFactory factory = PasswordFactory.getInstance(ScramDigestPassword.ALGORITHM_SCRAM_SHA_256, ELYTRON_PASSWORD_PROVIDERS);
            IteratedSaltedPasswordAlgorithmSpec algoSpec = new IteratedSaltedPasswordAlgorithmSpec(4096, salt);
            EncryptablePasswordSpec encSpec = new EncryptablePasswordSpec(userPassword.toCharArray(), algoSpec);
            ScramDigestPassword scramPassword = (ScramDigestPassword) factory.generatePassword(encSpec);
            IteratedSaltedHashPasswordSpec keySpec = factory.getKeySpec(scramPassword, IteratedSaltedHashPasswordSpec.class);

            preparedStatement.setString(1, userName);
            preparedStatement.setBytes(2, keySpec.getHash());
            preparedStatement.setBytes(3, keySpec.getSalt());
            preparedStatement.setInt(4, keySpec.getIterationCount());
            preparedStatement.execute();

            return keySpec;
        }
    }

    private String createBcryptModularCryptPasswordTable(String userName, String userPassword) throws Exception {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_bcrypt_modular_password");
            statement.executeUpdate("CREATE TABLE user_bcrypt_modular_password ( id INTEGER IDENTITY, name VARCHAR(100), password VARCHAR(100))");
        }

        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_bcrypt_modular_password (name, password) VALUES (?, ?)");
        ) {
            byte[] salt = generateRandomSalt(BCRYPT_SALT_SIZE);
            PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PASSWORD_PROVIDERS);
            BCryptPassword bCryptPassword = (BCryptPassword) passwordFactory.generatePassword(
                    new EncryptablePasswordSpec(userPassword.toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(10, salt))
            );
            String cryptString = ModularCrypt.encodeAsString(bCryptPassword);

            preparedStatement.setString(1, userName);
            preparedStatement.setString(2, cryptString);
            preparedStatement.execute();

            return cryptString;
        }
    }

    private void createBcryptPasswordTable(String userName, String userPassword, byte[] salt, int iterationCount) throws Exception {
        try (
                Connection connection = dataSourceRule.getDataSource().getConnection();
                Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_bcrypt_password");
            statement.executeUpdate("CREATE TABLE user_bcrypt_password ( id INTEGER IDENTITY, name VARCHAR(100), password OTHER, salt OTHER, iterationCount INTEGER)");
        }

        try (
                Connection connection = dataSourceRule.getDataSource().getConnection();
                PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_bcrypt_password (name, password, salt, iterationCount) VALUES (?, ?, ?, ?)");
        ) {
            PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PASSWORD_PROVIDERS);
            BCryptPassword bCryptPassword = (BCryptPassword) passwordFactory.generatePassword(
                    new EncryptablePasswordSpec(userPassword.toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt))
            );

            preparedStatement.setString(1, userName);
            preparedStatement.setBytes(2, bCryptPassword.getHash());
            preparedStatement.setBytes(3, bCryptPassword.getSalt());
            preparedStatement.setInt(4, bCryptPassword.getIterationCount());
            preparedStatement.execute();
        }
    }

    private void createBcryptEncodedTable(String userName, String userPassword, byte[] salt, int iterationCount) throws Exception {
        createBcryptEncodedTable(userName, userPassword, salt, iterationCount, StandardCharsets.UTF_8);
    }

    private void createBcryptEncodedTable(String userName, String userPassword, byte[] salt, int iterationCount, Charset hashCharset) throws Exception {
        try (
                Connection connection = dataSourceRule.getDataSource().getConnection();
                Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_bcrypt_encoded_password");
            statement.executeUpdate("CREATE TABLE user_bcrypt_encoded_password ( id INTEGER IDENTITY, name VARCHAR(100), password VARCHAR(100), salt VARCHAR(100), iterationCount VARCHAR(100))");
        }

        try (
                Connection connection = dataSourceRule.getDataSource().getConnection();
                PreparedStatement  preparedStatement = connection.prepareStatement("INSERT INTO user_bcrypt_encoded_password (name, password, salt, iterationCount) VALUES (?, ?, ?, ?)");
        ) {
            PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PASSWORD_PROVIDERS);
            BCryptPassword bCryptPassword = (BCryptPassword) passwordFactory.generatePassword(
                    new EncryptablePasswordSpec(userPassword.toCharArray(), new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt),
                            hashCharset));

            preparedStatement.setString(1, userName);
            preparedStatement.setString(2, ByteIterator.ofBytes(bCryptPassword.getHash()).hexEncode().drainToString());
            preparedStatement.setString(3, ByteIterator.ofBytes(bCryptPassword.getSalt()).base64Encode().drainToString());
            preparedStatement.setString(4, Integer.toString(bCryptPassword.getIterationCount()));
            preparedStatement.execute();
        }
    }

    private void createClearPasswordTable(String userName, String password) throws Exception {
        createClearPasswordTable();
        insertUserWithClearPassword(userName, password);
    }

    private void insertUserWithClearPassword(String userName, String password) throws SQLException {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("INSERT INTO user_clear_password (name, password) VALUES ('" + userName + "','" + password + "')");
        }
    }

    private void createClearPasswordTable() throws Exception {
        try (
            Connection connection = dataSourceRule.getDataSource().getConnection();
            Statement statement = connection.createStatement();
        ) {
            statement.executeUpdate("DROP TABLE IF EXISTS user_clear_password");
            statement.executeUpdate("CREATE TABLE user_clear_password ( id INTEGER IDENTITY, name VARCHAR(100), password VARCHAR(50))");
        }
    }

    private static byte[] generateRandomSalt(int saltSize) {
        byte[] randomSalt = new byte[saltSize];
        ThreadLocalRandom.current().nextBytes(randomSalt);
        return randomSalt;
    }
}