package cmonster.browsers;

import cmonster.cookies.Cookie;
import cmonster.cookies.DecryptedCookie;
import cmonster.cookies.EncryptedCookie;
import cmonster.utils.OS;
import com.sun.jna.platform.win32.Crypt32Util;
import org.apache.maven.shared.utils.io.DirectoryScanner;

import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.sql.*;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * An implementation of Chrome cookie decryption logic for Mac, Windows, and Linux installs
 * <p>
 * References:
 * 1) http://n8henrie.com/2014/05/decrypt-chrome-cookies-with-python/
 * 2) https://github.com/markushuber/ssnoob
 *
 * @author Ben Holland
 */
public class ChromeBrowser extends Browser {
	
	@Override
	public String getName() {
		return "Chrome";
	}

    private String chromeKeyringPassword = null;

    /**
     * Returns a set of cookie store locations
     */
    @Override
    protected Set<File> getCookieStores() {
        HashSet<File> cookieStores = new HashSet<>();
        String userHome = System.getProperty("user.home");

        String[] cookieDirectories = {
            "/AppData/Local/Google/Chrome/User Data",
            "/Application Data/Google/Chrome/User Data",
            "/Library/Application Support/Google/Chrome",
            "/.config/chromium"
        };

        for (String cookieDirectory : cookieDirectories) {
            String baseDir = userHome + cookieDirectory;
            String[] files = getCookieDbFiles(baseDir);
            if (files != null && files.length > 0) {
                for (String file : files) {
                    cookieStores.add(new File(baseDir + "/" + file));
                }
            }
        }

        return cookieStores;
    }

    /**
     * In come case, people could set profile for browsers, would create custom cookie files
     * @param baseDir
     * @author <a href="mailto:[email protected]">Albert Yu</a> 5/26/2017 1:40 PM
     */
    private String[] getCookieDbFiles(String baseDir) {
        String[] files = null;
        File filePath = new File(baseDir);
        if (filePath.exists() && filePath.isDirectory()) {
            DirectoryScanner ds = new DirectoryScanner();
            String[] includes = {"*/Cookies"};
            ds.setIncludes(includes);
            ds.setBasedir(new File(baseDir));
            ds.setCaseSensitive(true);
            ds.scan();
            files = ds.getIncludedFiles();
        }
        return files;
    }


    /**
     * Processes all cookies in the cookie store for a given domain or all
     * domains if domainFilter is null
     */
    @Override
    protected Set<Cookie> processCookies(File cookieStore, String domainFilter) {
        HashSet<Cookie> cookies = new HashSet<>();
        if (cookieStore.exists()) {
            Connection connection = null;
            try {
                cookieStoreCopy.delete();
                Files.copy(cookieStore.toPath(), cookieStoreCopy.toPath());
                // load the sqlite-JDBC driver using the current class loader
                Class.forName("org.sqlite.JDBC");
                // create a database connection
                connection = DriverManager.getConnection("jdbc:sqlite:" + cookieStoreCopy.getAbsolutePath());
                Statement statement = connection.createStatement();
                statement.setQueryTimeout(30); // set timeout to 30 seconds
                ResultSet result;
                if (domainFilter == null || domainFilter.isEmpty()) {
                    result = statement.executeQuery("select * from cookies");
                } else {
                    result = statement.executeQuery("select * from cookies where host_key like \"%" + domainFilter + "%\"");
                }
                while (result.next()) {
                    String name = result.getString("name");
                    parseCookieFromResult(cookieStore, name, cookies, result);
                }
            } catch (Exception e) {
                e.printStackTrace();
                // if the error message is "out of memory",
                // it probably means no database file is found
            } finally {
                try {
                    if (connection != null) {
                        connection.close();
                    }
                } catch (SQLException e) {
                    // connection close failed
                }
            }
        }
        return cookies;
    }

    /**
     * Returns cookies for cookie key with given domain
     */
    @Override
    public Set<Cookie> getCookiesForDomain(String name, String domain) {
        HashSet<Cookie> cookies = new HashSet<>();
        for(File cookieStore : getCookieStores()){
            cookies.addAll(getCookiesByName(cookieStore, name, domain));
        }
        return cookies;
    }

    private Set<Cookie> getCookiesByName(File cookieStore, String name, String domainFilter) {
        HashSet<Cookie> cookies = new HashSet<>();
        if (cookieStore.exists()) {
            Connection connection = null;
            try {
                cookieStoreCopy.delete();
                Files.copy(cookieStore.toPath(), cookieStoreCopy.toPath());
                // load the sqlite-JDBC driver using the current class loader
                Class.forName("org.sqlite.JDBC");
                // create a database connection
                connection = DriverManager.getConnection("jdbc:sqlite:" + cookieStoreCopy.getAbsolutePath());
                Statement statement = connection.createStatement();
                statement.setQueryTimeout(30); // set timeout to 30 seconds
                ResultSet result;
                if (domainFilter == null || domainFilter.isEmpty()) {
                    result = statement.executeQuery(String.format("select * from cookies where name = '%s'", name));
                } else {
                    result = statement.executeQuery("select * from cookies where name = '" + name + "' and host_key like '%" + domainFilter + "'");
                }
                while (result.next()) {
                    parseCookieFromResult(cookieStore, name, cookies, result);
                }
            } catch (Exception e) {
                e.printStackTrace();
                // if the error message is "out of memory",
                // it probably means no database file is found
            } finally {
                try {
                    if (connection != null) {
                        connection.close();
                    }
                } catch (SQLException e) {
                    // connection close failed
                }
            }
        }
        return cookies;
    }

    private void parseCookieFromResult(File cookieStore, String name, HashSet<Cookie> cookies, ResultSet result) throws SQLException {
        byte[] encryptedBytes = result.getBytes("encrypted_value");
        String path = result.getString("path");
        String domain = result.getString("host_key");
        boolean secure = determineSecure(result);
        boolean httpOnly = determineHttpOnly(result);
        Date expires = result.getDate("expires_utc");

        EncryptedCookie encryptedCookie = new EncryptedCookie(name,
            encryptedBytes,
            expires,
            path,
            domain,
            secure,
            httpOnly,
            cookieStore);

        DecryptedCookie decryptedCookie = decrypt(encryptedCookie);

        if (decryptedCookie != null) {
            cookies.add(decryptedCookie);
        } else {
            cookies.add(encryptedCookie);
        }
        cookieStoreCopy.delete();
    }

    private boolean determineHttpOnly(ResultSet result) throws SQLException {
        boolean secure;
        try {
            secure = result.getBoolean("is_httponly");
        } catch (SQLException e) {
            secure = result.getBoolean("httponly");
        }
        return secure;
    }
    
    private boolean determineSecure(ResultSet result) throws SQLException {
        boolean secure;
        try {
            secure = result.getBoolean("secure");
        } catch (SQLException e) {
            secure = result.getBoolean("is_secure");
        }
        return secure;
    }

    /**
     * Decrypts an encrypted cookie
     */
    @Override
    protected DecryptedCookie decrypt(EncryptedCookie encryptedCookie) {
        byte[] decryptedBytes = null;
        if (OS.isWindows()) {
            try {
                decryptedBytes = Crypt32Util.cryptUnprotectData(encryptedCookie.getEncryptedValue());
            } catch (Exception e) {
                decryptedBytes = null;
            }
        } else if (OS.isLinux()) {
            try {
                byte[] salt = "saltysalt".getBytes();
                char[] password = "peanuts".toCharArray();
                char[] iv = new char[16];
                Arrays.fill(iv, ' ');
                int keyLength = 16;

                int iterations = 1;

                PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength * 8);
                SecretKeyFactory pbkdf2 = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

                byte[] aesKey = pbkdf2.generateSecret(spec).getEncoded();

                SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");

                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(new String(iv).getBytes()));

                // if cookies are encrypted "v10" is a the prefix (has to be removed before decryption)
                byte[] encryptedBytes = encryptedCookie.getEncryptedValue();
                if (new String(encryptedCookie.getEncryptedValue()).startsWith("v10")) {
                    encryptedBytes = Arrays.copyOfRange(encryptedBytes, 3, encryptedBytes.length);
                }
                decryptedBytes = cipher.doFinal(encryptedBytes);
            } catch (Exception e) {
                decryptedBytes = null;
            }
        } else if (OS.isMac()) {
            // access the decryption password from the keyring manager
            if (chromeKeyringPassword == null) try {
                chromeKeyringPassword = getMacKeyringPassword("Chrome Safe Storage");
            } catch (IOException ignored) {
            }
            try {
                byte[] salt = "saltysalt".getBytes();
                char[] password = chromeKeyringPassword.toCharArray();
                char[] iv = new char[16];
                Arrays.fill(iv, ' ');
                int keyLength = 16;

                int iterations = 1003;

                PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength * 8);
                SecretKeyFactory pbkdf2 = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

                byte[] aesKey = pbkdf2.generateSecret(spec).getEncoded();

                SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");

                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(new String(iv).getBytes()));

                // if cookies are encrypted "v10" is a the prefix (has to be removed before decryption)
                byte[] encryptedBytes = encryptedCookie.getEncryptedValue();
                if (new String(encryptedCookie.getEncryptedValue()).startsWith("v10")) {
                    encryptedBytes = Arrays.copyOfRange(encryptedBytes, 3, encryptedBytes.length);
                }
                decryptedBytes = cipher.doFinal(encryptedBytes);
            } catch (Exception e) {
                decryptedBytes = null;
            }
        }

        if (decryptedBytes == null) {
            return null;
        } else {
            return new DecryptedCookie(encryptedCookie.getName(),
                encryptedCookie.getEncryptedValue(),
                new String(decryptedBytes),
                encryptedCookie.getExpires(),
                encryptedCookie.getPath(),
                encryptedCookie.getDomain(),
                encryptedCookie.isSecure(),
                encryptedCookie.isHttpOnly(),
                encryptedCookie.getCookieStore());
        }
    }

    /**
     * Accesses the apple keyring to retrieve the Chrome decryption password
     *
     * @param application
     * @return
     * @throws IOException
     */
    private static String getMacKeyringPassword(String application) throws IOException {
        Runtime rt = Runtime.getRuntime();
        String[] commands = {"security", "find-generic-password", "-w", "-s", application};
        Process proc = rt.exec(commands);
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
        StringBuilder result = new StringBuilder();
        String s;
        while ((s = stdInput.readLine()) != null) {
            result.append(s);
        }
        return result.toString();
    }

}