Skip to content

Commit 08b2caa

Browse files
committed
Handle hashcat status line less rigidly
In Hashcat 7.0.0, there is an extra field POWER which tells you how much power each device is drawing. This currently breaks the Hashtopia agent-python when using 7.0.0 because it expects the end of the line to be integer values for UTIL. In order to avoid this problem in the future, be less rigid in parsing how the status line is returned. Print out messages for any unexpected fields.
1 parent 211b728 commit 08b2caa

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)