Skip to content

Commit fab55c6

Browse files
authored
Merge pull request #58 from kruton/hashcat7
Support Hashcat 7.0.0 with less-rigid status parsing
2 parents 211b728 + 08b2caa commit fab55c6

File tree

2 files changed

+168
-38
lines changed

2 files changed

+168
-38
lines changed

htpclient/hashcat_status.py

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
class HashcatStatus:
22
def __init__(self, line):
3-
# parse
3+
"""
4+
Initializes the HashcatStatus object by parsing a machine-readable
5+
status line from Hashcat.
6+
7+
Args:
8+
line (str): A single line of Hashcat machine-readable status
9+
output.
10+
"""
411
self.status = -1
512
self.speed = []
613
self.exec_runtime = []
@@ -11,44 +18,72 @@ def __init__(self, line):
1118
self.rejected = 0
1219
self.util = []
1320
self.temp = []
21+
self.power = []
22+
self.unknown_fields = False
23+
24+
try:
25+
fields = line.strip().split('\t')
26+
if not fields or fields[0] != 'STATUS':
27+
# Not a valid status line
28+
return
29+
30+
self.status = int(fields[1])
31+
32+
i = 2
33+
while i < len(fields):
34+
key = fields[i]
35+
i += 1
1436

15-
line = line.split("\t")
16-
if line[0] != "STATUS":
17-
# invalid line
18-
return
19-
elif len(line) < 19:
20-
# invalid line
21-
return
22-
self.status = int(line[1])
23-
index = 3
24-
while line[index] != "EXEC_RUNTIME":
25-
self.speed.append([int(line[index]), int(line[index + 1])])
26-
index += 2
27-
while line[index] != "CURKU":
28-
index += 1
29-
self.curku = int(line[index + 1])
30-
self.progress[0] = int(line[index + 3])
31-
self.progress[1] = int(line[index + 4])
32-
self.rec_hash[0] = int(line[index + 6])
33-
self.rec_hash[1] = int(line[index + 7])
34-
self.rec_salt[0] = int(line[index + 9])
35-
self.rec_salt[1] = int(line[index + 10])
36-
if line[index + 11] == "TEMP":
37-
# we have temp values
38-
index += 12
39-
while line[index] != "REJECTED":
40-
self.temp.append(int(line[index]))
41-
index += 1
42-
else:
43-
index += 11
44-
self.rejected = int(line[index + 1])
45-
if len(line) > index + 2:
46-
index += 2
47-
if line[index] == "UTIL":
48-
index += 1
49-
while len(line) - 1 > index: # -1 because the \r\n is also included in the split
50-
self.util.append(int(line[index]))
51-
index += 1
37+
if key == 'SPEED':
38+
# Speed has two values per device: hashes over period and period in ms
39+
while i + 1 < len(fields) and fields[i].isdigit() and fields[i+1].isdigit():
40+
self.speed.append([int(fields[i]), int(fields[i+1])])
41+
i += 2
42+
elif key == 'EXEC_RUNTIME':
43+
# Execution runtime per device
44+
while i < len(fields) and fields[i].replace('.', '', 1).isdigit():
45+
self.exec_runtime.append(float(fields[i]))
46+
i += 1
47+
elif key == 'CURKU':
48+
self.curku = int(fields[i])
49+
i += 1
50+
elif key == 'PROGRESS':
51+
# Progress has two values: current and total
52+
self.progress = [int(fields[i]), int(fields[i+1])]
53+
i += 2
54+
elif key == 'RECHASH':
55+
# Recovered hashes has two values: done and total
56+
self.rec_hash = [int(fields[i]), int(fields[i+1])]
57+
i += 2
58+
elif key == 'RECSALT':
59+
# Recovered salts has two values: done and total
60+
self.rec_salt = [int(fields[i]), int(fields[i+1])]
61+
i += 2
62+
elif key == 'TEMP':
63+
# Temperature per device
64+
while i < len(fields) and fields[i].lstrip('-').isdigit():
65+
self.temp.append(int(fields[i]))
66+
i += 1
67+
elif key == 'REJECTED':
68+
self.rejected = int(fields[i])
69+
i += 1
70+
elif key == 'UTIL':
71+
# Utilization per device
72+
while i < len(fields) and fields[i].lstrip('-').isdigit():
73+
self.util.append(int(fields[i]))
74+
i += 1
75+
elif key == 'POWER':
76+
# Power usage per device (newer versions)
77+
while i < len(fields) and fields[i].lstrip('-').isdigit():
78+
self.power.append(int(fields[i]))
79+
i += 1
80+
else:
81+
print(f"Unknown field in Hashcat status line: {key}")
82+
self.unknown_fields = True
83+
pass
84+
except (ValueError, IndexError) as e:
85+
print(f"Error parsing Hashcat status line: {e}")
86+
self.__init__("") # Fallback to default initialization
5287

5388
def is_valid(self):
5489
return self.status >= 0
@@ -87,3 +122,14 @@ def get_speed(self):
87122

88123
def get_rejected(self):
89124
return self.rejected
125+
126+
def get_all_power(self):
127+
return self.power
128+
129+
def get_power(self):
130+
if not self.power:
131+
return -1
132+
power_sum = 0
133+
for p in self.power:
134+
power_sum += p
135+
return int(power_sum / len(self.power))

tests/test_hashcat_status.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import unittest
2+
from htpclient.hashcat_status import HashcatStatus
3+
4+
class TestHashcatStatus(unittest.TestCase):
5+
def test_hashcat_6_single_device(self):
6+
line = "STATUS\t3\tSPEED\t11887844\t1000\tEXEC_RUNTIME\t15.870873\tCURKU\t170970511093\tPROGRESS\t2735618289488\t2736891330000\tRECHASH\t0\t1\tRECSALT\t0\t1\tTEMP\t-1\tREJECTED\t0\tUTIL\t100\t"
7+
status = HashcatStatus(line)
8+
self.assertTrue(status.is_valid())
9+
self.assertEqual(status.status, 3)
10+
self.assertEqual(status.speed, [[11887844, 1000]])
11+
self.assertEqual(status.exec_runtime, [15.870873])
12+
self.assertEqual(status.curku, 170970511093)
13+
self.assertEqual(status.progress, [2735618289488, 2736891330000])
14+
self.assertEqual(status.rec_hash, [0, 1])
15+
self.assertEqual(status.rec_salt, [0, 1])
16+
self.assertEqual(status.temp, [-1])
17+
self.assertEqual(status.rejected, 0)
18+
self.assertEqual(status.util, [100])
19+
self.assertEqual(status.power, [])
20+
self.assertEqual(status.unknown_fields, False)
21+
22+
def test_hashcat_7_single_device(self):
23+
line = "STATUS\t3\tSPEED\t11887844\t1000\tEXEC_RUNTIME\t15.870873\tCURKU\t170970511093\tPROGRESS\t2735618289488\t2736891330000\tRECHASH\t0\t1\tRECSALT\t0\t1\tTEMP\t-1\tREJECTED\t0\tUTIL\t100\tPOWER\t56\t"
24+
status = HashcatStatus(line)
25+
self.assertTrue(status.is_valid())
26+
self.assertEqual(status.status, 3)
27+
self.assertEqual(status.speed, [[11887844, 1000]])
28+
self.assertEqual(status.exec_runtime, [15.870873])
29+
self.assertEqual(status.curku, 170970511093)
30+
self.assertEqual(status.progress, [2735618289488, 2736891330000])
31+
self.assertEqual(status.rec_hash, [0, 1])
32+
self.assertEqual(status.rec_salt, [0, 1])
33+
self.assertEqual(status.temp, [-1])
34+
self.assertEqual(status.rejected, 0)
35+
self.assertEqual(status.util, [100])
36+
self.assertEqual(status.power, [56])
37+
self.assertEqual(status.unknown_fields, False)
38+
39+
def test_valid_status_line(self):
40+
line = "STATUS\t1\tSPEED\t2534\t1000\tEXEC_RUNTIME\t123\tCURKU\t45\tPROGRESS\t67\t100\tRECHASH\t89\t120\tRECSALT\t56\t110\tTEMP\t25\tREJECTED\t7\tUTIL\t85\t90\tPOWER\t100\t150"
41+
status = HashcatStatus(line)
42+
self.assertEqual(status.status, 1)
43+
self.assertEqual(status.speed, [[2534, 1000]])
44+
self.assertEqual(status.exec_runtime, [123])
45+
self.assertEqual(status.curku, 45)
46+
self.assertEqual(status.progress, [67, 100])
47+
self.assertEqual(status.rec_hash, [89, 120])
48+
self.assertEqual(status.rec_salt, [56, 110])
49+
self.assertEqual(status.temp, [25])
50+
self.assertEqual(status.rejected, 7)
51+
self.assertEqual(status.util, [85, 90])
52+
self.assertEqual(status.power, [100, 150])
53+
54+
def test_invalid_status_line(self):
55+
line = "NOT_STATUS_LINE"
56+
status = HashcatStatus(line)
57+
self.assertEqual(status.status, -1)
58+
59+
def test_missing_fields(self):
60+
line = "STATUS\t1\tSPEED\t200\t1000"
61+
status = HashcatStatus(line)
62+
self.assertEqual(status.status, 1)
63+
self.assertEqual(status.speed, [[200, 1000]])
64+
self.assertEqual(status.exec_runtime, [])
65+
self.assertEqual(status.curku, 0)
66+
self.assertEqual(status.progress, [0, 0])
67+
68+
def test_get_progress(self):
69+
line = "STATUS\t1\tPROGRESS\t42\t100"
70+
status = HashcatStatus(line)
71+
self.assertEqual(status.get_progress(), 42)
72+
73+
def test_get_speed(self):
74+
line = "STATUS\t1\tSPEED\t12400\t1000\t2000\t1000"
75+
status = HashcatStatus(line)
76+
self.assertEqual(status.get_speed(), 12400 + 2000)
77+
78+
def test_get_util(self):
79+
line = "STATUS\t1\tUTIL\t85\t90"
80+
status = HashcatStatus(line)
81+
self.assertEqual(status.get_util(), (85 + 90) // 2)
82+
83+
if __name__ == '__main__':
84+
unittest.main()

0 commit comments

Comments
 (0)