@@ -341,7 +341,6 @@ def _try_sae_capture(self):
341341 return False # Let it fall through to passive
342342 handshake = self .capture_sae_handshake ()
343343 if handshake :
344- Color .pl ('{+} {G}SAE handshake captured successfully{W}' )
345344 self ._finalize_sae_success (handshake , key = None )
346345 return True
347346 Color .pl ('{!} {O}SAE handshake capture failed{W}' )
@@ -373,21 +372,49 @@ def _try_passive(self):
373372 self .view .add_log ('Starting passive capture - final fallback' )
374373 handshake = self .passive_capture ()
375374 if handshake :
376- Color .pl ('{+} {G}SAE handshake captured passively{W}' )
377375 self ._finalize_sae_success (handshake , key = None )
378376 return True
379377 return False
380378
381379 def _finalize_sae_success (self , handshake , key ):
382- """Wrap a captured SAE handshake in CrackResultSAE and set success .
380+ """Record a captured SAE handshake as a CrackResultSAE .
383381
384382 `handshake` is an SAEHandshake object (has .capfile / .bssid / .essid).
385- `key` is the cracked PSK if known, else None.
383+ `key` is the recovered PSK if known, else None.
384+
385+ When `key` is None we still save the capture (for records / later
386+ analysis) but we must NOT present it as a crackable win — see
387+ _warn_sae_not_offline_crackable for why.
386388 """
387389 self .crack_result = CrackResultSAE (
388390 handshake .bssid , handshake .essid , handshake .capfile , key )
389391 self .crack_result .dump ()
390392 self .success = True
393+ if key is None :
394+ self ._warn_sae_not_offline_crackable ()
395+
396+ def _warn_sae_not_offline_crackable (self ):
397+ """Honest caveat: a captured SAE handshake is NOT offline-crackable.
398+
399+ Unlike a WPA2 4-way handshake, WPA3-SAE (Dragonfly) is a PAKE
400+ designed to resist offline dictionary attacks. Capturing the SAE
401+ exchange does not let hashcat/aircrack recover the password the way
402+ a WPA2 capture does — there is no hashcat mode that cracks a captured
403+ SAE handshake. We keep the capture, but we tell the user the truth
404+ instead of implying the network was owned.
405+ """
406+ Color .pl ('{!} {O}SAE handshake captured and saved — but note:{W}' )
407+ Color .pl ('{!} {O}WPA3-SAE (Dragonfly) resists offline dictionary attacks.{W}' )
408+ Color .pl ('{!} {O}A captured SAE handshake {R}cannot be cracked offline{O} '
409+ 'like a WPA2 handshake.{W}' )
410+ Color .pl ('{!} {O}Offline password recovery is only feasible via:{W}' )
411+ Color .pl (' {C}-{W} Transition-mode {C}downgrade{W} to WPA2 '
412+ '(captures a crackable 4-way handshake)' )
413+ Color .pl (' {C}-{W} A successful {C}Dragonblood timing{W} partition '
414+ '(only MODP groups 22/23/24)' )
415+ if self .view :
416+ self .view .add_log ('SAE handshake captured (NOT offline-crackable — '
417+ 'WPA3-SAE resists dictionary attacks)' )
391418
392419 def _active_probe_for_vulnerable_groups (self ):
393420 """
@@ -791,9 +818,17 @@ def attempt_downgrade(self):
791818 return None
792819
793820 self .clients = []
794-
795- # Set timeout for downgrade attempt (30 seconds as per requirements)
796- downgrade_timeout = Timer (30 )
821+
822+ # Set timeout for the downgrade attempt. Honour --wpa3-timeout when
823+ # the user set it; otherwise keep a short 30s window — the downgrade
824+ # is a fast first attempt that falls back to SAE capture, so it
825+ # shouldn't occupy the full wpa_attack_timeout (300s) by default.
826+ timeout_value = Configuration .wpa3_attack_timeout or 30
827+ downgrade_timeout = Timer (timeout_value )
828+ # Warn once if no clients appear partway through (the downgrade
829+ # needs active clients). Half the window, floored so short timeouts
830+ # still produce a warning.
831+ no_clients_warn_after = max (5 , timeout_value // 2 )
797832 deauth_timer = Timer (Configuration .wpa_deauth_timeout )
798833 sae_detected = False
799834 deauth_attempts = 0
@@ -811,7 +846,7 @@ def attempt_downgrade(self):
811846
812847 # Calculate progress percentage
813848 elapsed = downgrade_timeout .running_time ()
814- total_time = 30 # 30 second timeout
849+ total_time = timeout_value
815850 progress_pct = min (100 , int ((elapsed / total_time ) * 100 ))
816851
817852 # Update TUI view if available
@@ -848,9 +883,11 @@ def attempt_downgrade(self):
848883 Color .pattack ('WPA3' , self .target , 'Downgrade' ,
849884 'Waiting for clients... (%s)' % downgrade_timeout )
850885
851- # Show warning after some time with no clients
852- if not no_clients_warning_shown and downgrade_timeout .running_time () > 30 :
853- Color .pl ('\n {!} {O}No clients detected after 30 seconds{W}' )
886+ # Show warning once we're partway through with no clients
887+ if not no_clients_warning_shown and \
888+ downgrade_timeout .running_time () >= no_clients_warn_after :
889+ Color .pl ('\n {!} {O}No clients detected after %d seconds{W}'
890+ % no_clients_warn_after )
854891 Color .pl ('{!} {O}Downgrade requires active clients to succeed{W}' )
855892 if self .view :
856893 self .view .add_log ('Warning: No clients detected' )
@@ -899,7 +936,7 @@ def attempt_downgrade(self):
899936 # After deauth, clients should reconnect with WPA2
900937 # Check hcxdumptool capture (pcapng format)
901938 try :
902- if hcxdump and hcxdump .has_captured_data ():
939+ if hcxdump and hcxdump .has_new_data ():
903940 handshake = Handshake (hcxdump .get_output_file (),
904941 bssid = self .target .bssid ,
905942 essid = self .target .essid )
@@ -1082,7 +1119,7 @@ def capture_sae_handshake(self):
10821119
10831120 # Check if we've captured data
10841121 try :
1085- if hcxdump .has_captured_data ():
1122+ if hcxdump .has_new_data ():
10861123 # Create SAEHandshake object
10871124 sae_hs = SAEHandshake (
10881125 hcxdump .get_output_file (),
@@ -1253,7 +1290,7 @@ def passive_capture(self):
12531290
12541291 # Check if we've captured data
12551292 try :
1256- if hcxdump .has_captured_data ():
1293+ if hcxdump .has_new_data ():
12571294 # Create SAEHandshake object
12581295 sae_hs = SAEHandshake (
12591296 hcxdump .get_output_file (),
0 commit comments