 
/*  AccountAPI
 *
 *  Copyright (C) 2023  Rene Pawlitzek, Robert Schoech
 *  
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import org.apache.commons.codec.binary.Hex;
import org.sqlite.SQLite;
import org.sqlite.SQLiteDataSource;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AccountAPI {
    
    //private final String url = "jdbc:sqlite:C:/temp/db.sqlite";
    private final String url = "jdbc:sqlite:" + System.getProperty("user.dir") + "/data/db.sqlite";
    private final String key ="aesEncryptionKey";
    private final String initVector = "encryptionIntVec";

    public AccountAPI() { 
        // init Account-DB
        init();
    }
    
    public String getSecretKey(String name) {
        try {
            String hashedSec = getDBValue(name, 4);
            return decrypt(hashedSec);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }
    
    public String getCompany(String name) {
        try {
             return getDBValue(name, 2);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }
    
    public boolean verifyAccount(String name) {
        try (Connection conn = DriverManager.getConnection(url)) {
            if (conn != null) {            
                 var stmt = conn.createStatement();
                var sql = "SELECT * FROM User WHERE p_name == '" + name + "'"; 

                ResultSet rs = stmt.executeQuery(sql);
                try {
                    String exS = rs.getString(1);       
                    System.out.println("Name " + exS + " exists.");
                    stmt.close();
                    conn.close();
                    return true;
                } catch (SQLException e) {
                    stmt.close();
                    System.out.println("Name NOT exists.");
                }
            }
            conn.close();
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
        return false;
    }
    
    public void init() {

        try (var conn = DriverManager.getConnection(url)) {
            if (conn != null) {
                var meta = conn.getMetaData();
                System.out.println("The driver name is " + meta.getDriverName());
                
                var sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='User';";
                var stmt = conn.createStatement();
                var rs = stmt.executeQuery(sql);
                try {
                    var exS = rs.getString(1);       
                    System.out.println("Table " + exS + " exists.");        
                }
                catch (SQLException e) {
                    System.out.println("Table 'User' not exists.");
                    
                    sql = "CREATE TABLE User " +

                    "(p_name TEXT PRIMARY KEY NOT NULL," +

                    " p_company TEXT NOT NULL, " +

                    " p_pw TEXT NOT NULL, " +
                    
                    " p_sec TEXT NOT NULL, " +

                    " p_id TEXT NOT NULL) " ;

                    stmt.executeUpdate(sql);
                    System.out.println("A new table 'User' has been created.");
                }
                stmt.close();
                conn.close();
            }

        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
    }
    
    public void addAccount(String name, String company, String pw, String sec) {
        try (Connection conn = DriverManager.getConnection(url)) {
            String salt = generateSalt();
            String pepper = readPepper();
            String hashedpw = generateHashForPassword(pw, salt, pepper);
            String cipherSec = encrypt(sec);

            if (conn != null) {            
                conn.setAutoCommit(false);
                 var stmt = conn.createStatement();

                var sql = "INSERT INTO User (p_name,p_company,p_pw,p_sec,p_id) " +

                "VALUES ('" +name+ "', + '" +company+ "', + '" +hashedpw+ "', + '" +cipherSec+ "', + '" +salt+ "')";

                stmt.executeUpdate(sql);

                stmt.close();
                conn.commit();
                conn.close();
            }
            System.out.println("Insert is done");
            
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        } 
    }
    
    public boolean verifyPassword(String name, String pw) {
        String hashedPassword = getDBValue(name, 3);
        String salt = getDBValue(name, 5);
        String pepper = readPepper();
        return verify(hashedPassword, pw, salt, pepper);
    }
    
    private String decrypt(String value) {
        try {
            IvParameterSpec iv;
            iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] cipherText = cipher.doFinal(decoder.decode(value));
            
            return new String(cipherText);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return null;
        }
    }
    
    private String encrypt(String value) {
        try {
            var iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] cipherText = cipher.doFinal(value.getBytes("UTF-8"));
            Base64.Encoder encoder = Base64.getEncoder();
            return encoder.encodeToString(cipherText);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return null;
        }
    }
    
    private String generateHashForPassword(String password, String salt, String pepper) {
        try {
        
            MessageDigest md;
            // Select the message digest for the hash computation -> SHA-256
            md = MessageDigest.getInstance("SHA-512");
        
            // Passing the salt to the digest for the computation
            md.update(salt.getBytes(StandardCharsets.UTF_8));

            // Add the pepper
            md.update(pepper.getBytes(StandardCharsets.UTF_8));
            
            // Generate the salted hash
            byte[] hashedPassword = md.digest(password.getBytes(StandardCharsets.UTF_8));
            return Hex.encodeHexString(hashedPassword);
        } catch (NoSuchAlgorithmException e) {
            System.out.println(e.getMessage());
            return null;
        }
    }
    
    private String generateSalt() {
        // Generate the random salt
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return Hex.encodeHexString(salt);
    }
    
    private String getDBValue(String name, int index) {
        try (Connection conn = DriverManager.getConnection(url)) {
            if (conn != null) {            
                 var stmt = conn.createStatement();
                var sql = "SELECT * FROM User WHERE p_name == '" + name + "'"; 
                var rs = stmt.executeQuery(sql);
                try {
                    var exS = rs.getString(index);  
                    stmt.close();
                    conn.close();
                    return exS;
                } catch (SQLException e) {
                    stmt.close();
                }
            }
            conn.close();
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
        return null;
    }
    
    private String readPepper() {
        try (var conn = DriverManager.getConnection(url)) {
            if (conn != null) {
                var meta = conn.getMetaData();
                System.out.println("The driver name is " + meta.getDriverName());
                
                var sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='User';";
                var stmt = conn.createStatement();
                var rs = stmt.executeQuery(sql);
                try {
                    var exS = rs.getString(1);
                    stmt.close();
                    conn.close();
                    return exS;        
                } catch (SQLException e) {
                    stmt.close();
                }
                conn.close();
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
        return null;    
   }    

    private boolean verify(String hash, String password, String salt, String pepper) {
        var hashedPassword = generateHashForPassword(password, salt, pepper);
        return hash.equals(hashedPassword);
    }
}
