diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 4d46245..4acb2dd 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -557,6 +557,11 @@ def passgpt_attack(ctx: Any) -> None: if choice.upper() == "T": print("\n\tTrain a new PassGPT model") + print("\n\t--- Estimated Training Times (14M passwords, 3 epochs) ---") + print("\t CUDA (RTX 3090/4090): 1-3 hours") + print("\t MPS (Apple Silicon): 6-12 hours") + print("\t CPU: Very slow (not recommended)") + print("\t Use --max-lines to reduce training data for faster runs.") training_file = ctx.select_file_with_autocomplete( "Select training wordlist", base_dir=ctx.hcatWordlists ) @@ -568,7 +573,16 @@ def passgpt_attack(ctx: Any) -> None: base = input(f"\n\tBase model ({default_model}): ").strip() if not base: base = default_model - result = ctx.hcatPassGPTTrain(training_file, base) + + print("\n\tSelect training device:") + print("\t (1) cuda (Recommended)") + print("\t (2) mps (Apple Silicon)") + print("\t (3) cpu") + device_choice = input("\n\tDevice [1]: ").strip() + device_map = {"1": "cuda", "2": "mps", "3": "cpu", "": "cuda"} + device = device_map.get(device_choice, "cuda") + + result = ctx.hcatPassGPTTrain(training_file, base, device=device) if result is None: print("\n\tTraining failed. Returning to menu.") return diff --git a/hate_crack/main.py b/hate_crack/main.py index 6108bc2..cd61fa3 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -2297,7 +2297,7 @@ def _passgpt_model_dir(): # PassGPT Attack - Fine-tune a model on a custom wordlist -def hcatPassGPTTrain(training_file, base_model=None): +def hcatPassGPTTrain(training_file, base_model=None, device=None): training_file = os.path.abspath(training_file) if not os.path.isfile(training_file): print(f"Error: Training file not found: {training_file}") @@ -2321,6 +2321,8 @@ def hcatPassGPTTrain(training_file, base_model=None): "--output-dir", output_dir, ] + if device: + cmd.extend(["--device", device]) print(f"[*] Running: {_format_cmd(cmd)}") proc = subprocess.Popen(cmd) try: diff --git a/tests/test_passgpt_attack.py b/tests/test_passgpt_attack.py index 63fe8a6..517caa0 100644 --- a/tests/test_passgpt_attack.py +++ b/tests/test_passgpt_attack.py @@ -258,8 +258,8 @@ class TestPassGPTAttackHandler: ctx.select_file_with_autocomplete.return_value = "/tmp/wordlist.txt" ctx.hcatPassGPTTrain.return_value = "/home/user/.hate_crack/passgpt/wordlist" - # "T" for train, "" for default base model, "" for default max candidates - inputs = iter(["T", "", ""]) + # "T" for train, "" for default base model, "" for default device (cuda), "" for default max candidates + inputs = iter(["T", "", "", ""]) with ( patch("builtins.input", side_effect=inputs), patch("hate_crack.attacks.os.path.isdir", return_value=False), @@ -269,7 +269,7 @@ class TestPassGPTAttackHandler: passgpt_attack(ctx) ctx.hcatPassGPTTrain.assert_called_once_with( - "/tmp/wordlist.txt", "javirandor/passgpt-10characters" + "/tmp/wordlist.txt", "javirandor/passgpt-10characters", device="cuda" ) ctx.hcatPassGPT.assert_called_once() call_kwargs = ctx.hcatPassGPT.call_args @@ -280,7 +280,8 @@ class TestPassGPTAttackHandler: ctx.select_file_with_autocomplete.return_value = "/tmp/wordlist.txt" ctx.hcatPassGPTTrain.return_value = None - inputs = iter(["T", ""]) + # "T" for train, "" for default base model, "" for default device (cuda) + inputs = iter(["T", "", ""]) with ( patch("builtins.input", side_effect=inputs), patch("hate_crack.attacks.os.path.isdir", return_value=False),