#!/usr/bin/env python
"""
Script to brute force the passphrase of a Java KeyStore (JKS) file 
including passphrase of private keys if different from the JKS one.

If the script cannot recover the passphrase of a private key then use this script:
https://github.com/righettod/toolbox-pentest-web/blob/master/scripts/generate-jks-private-keys-hashes-for-hashcat.sh

Dependency: pip install termcolor pyjks
"""
import argparse
import jks
import concurrent.futures
from termcolor import colored
from queue import Queue

STATUS_QUEUE = Queue()


def try_recover_jks_passphrase(tgt_jks_file, passphrase):
    try:
        if STATUS_QUEUE.empty():
            jks.KeyStore.load(tgt_jks_file, passphrase)
            STATUS_QUEUE.put(passphrase)
    except:
        pass


def try_recover_private_key_passphrase(private_key, passphrase):
    try:
        if STATUS_QUEUE.empty() and not private_key.is_decrypted():
            private_key.decrypt(passphrase)
            if private_key.is_decrypted():
                STATUS_QUEUE.put(passphrase)
    except:
        pass


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Script to brute force the passphrase of a Java KeyStore (JKS) file.")
    required_params = parser.add_argument_group("required named arguments")
    required_params.add_argument("-f", action="store", dest="jks_file", help="Path to the JKS file.", required=True)
    required_params.add_argument("-d", action="store", dest="passphrase_dict", help="Path to the passphrase dictionary.", required=True)
    parser.add_argument("-t", action="store", dest="thread_count", help="Number of parallel threads to use (default to 10)", required=False, default="10")
    args = parser.parse_args()
    keystore_file = args.jks_file
    print(colored(f"[+] Loading the passphrases collection...", "yellow"))
    with open(args.passphrase_dict, mode="r", encoding="utf-8") as f:
        passphrases = f.read().splitlines()
        passphrases_count = len(passphrases)
    print(f"{passphrases_count} passphrases loaded.")
    print(colored(f"[+] Launching the brute force operation using {args.thread_count} threads...", "yellow"))
    with concurrent.futures.ThreadPoolExecutor(max_workers=int(args.thread_count)) as executor:
        index = 0
        formatting_length = len(str(passphrases_count))
        for p in passphrases:
            if STATUS_QUEUE.empty():
                index += 1
                progress = f"{str(index).zfill(formatting_length)} / {passphrases_count}"
                print(f"\r{progress}", end="", flush=True)
                executor.submit(try_recover_jks_passphrase, tgt_jks_file=keystore_file, passphrase=p)
            else:
                break
    if STATUS_QUEUE.empty():
        print(colored(f"\n[!] Passphrase not found.", "red"))
    else:
        passphrase = STATUS_QUEUE.get()
        keystore = jks.KeyStore.load(keystore_file, passphrase)
        print(colored(f"\n[V] Passphrase found.", "green"))
        print(f"Passphrase        : {passphrase}")
        print(f"Private keys count: {len(keystore.private_keys)}")
        print(f"Certiticates count: {len(keystore.certs)}")
        print(f"Secret keys  count: {len(keystore.secret_keys)}")
        for private_key_alias in keystore.private_keys:
            key = keystore.private_keys[private_key_alias]
            if not key.is_decrypted():
                print(f"Private key '{private_key_alias}' have a passphrase different from the JKS one!\nTry recovering the passphrase using the provided dictionary...")
                index = 0
                for p in passphrases:
                    if STATUS_QUEUE.empty():
                        index += 1
                        progress = f"{str(index).zfill(formatting_length)} / {passphrases_count}"
                        print(f"\r{progress}", end="", flush=True)
                        with concurrent.futures.ThreadPoolExecutor(max_workers=int(args.thread_count)) as executor:
                            executor.submit(try_recover_private_key_passphrase, private_key=key, passphrase=p)
                    else:
                        break
                if STATUS_QUEUE.empty():
                    print(colored(f"\r[!] Passphrase not found.", "red"))
                else:
                    passphrase = STATUS_QUEUE.get()
                    print(colored(f"\r[V] Passphrase found: {passphrase}", "green"))
