From 61b197f10e6bebb49926ddf08230fc4f2f98a957 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 21 Jul 2026 17:56:27 -0400 Subject: [PATCH] fix(hashview): correct hash-type parsing for MD5 and hashfile listing get_hashfile_details used 'or' fallthrough on hash_type, so MD5 (hash_type 0, falsy) resolved to the envelope 'type' string ('message') instead of 0. Select by key presence and drop the bogus 'type' fallback. get_hashfile_hash_type looked for file_ids/ids/hashfile_ids keys that the endpoint never returns; it always yielded []. Read the actual 'hashfiles' envelope array and extract each file id. Co-Authored-By: Claude Opus 4.8 --- hate_crack/api.py | 29 ++++++++++++++++++++-------- tests/test_hashview.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/hate_crack/api.py b/hate_crack/api.py index eecffc0..52b7345 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1138,7 +1138,13 @@ class HashviewAPI: data = None hashtype = None if data: - hashtype = data.get("hashtype") or data.get("hash_type") or data.get("type") + # Prefer explicit hash-type keys by PRESENCE, not truthiness: MD5 is + # hash_type 0, which is falsy. Never fall back to `type` — that is + # the envelope tag ("message"), never a hash mode. + if "hash_type" in data: + hashtype = data["hash_type"] + elif "hashtype" in data: + hashtype = data["hashtype"] if self.debug: print( f"[DEBUG] get_hashfile_details({hashfile_id}): raw data={data}, hashtype={hashtype}" @@ -1660,21 +1666,28 @@ class HashviewAPI: def get_hashfile_hash_type(self, hashtype_id): """ Query /v1/hashfiles/hash_type/ and return a list of file IDs. + + The endpoint answers with an envelope ``{..., "hashfiles": [ {...} ]}`` + (native JSON objects); we extract the ``id`` of each hashfile. """ url = f"{self.base_url}/v1/hashfiles/hash_type/{hashtype_id}" resp = self.session.get(url) resp.raise_for_status() try: data = resp.json() - # Expecting a list of file IDs or a dict with a key containing them + # A bare list is tolerated for forward/backward compatibility. if isinstance(data, list): - return data + hashfiles = data elif isinstance(data, dict): - # Try common keys - for key in ("file_ids", "ids", "hashfile_ids"): - if key in data and isinstance(data[key], list): - return data[key] - return [] + hashfiles = data.get("hashfiles") or [] + else: + return [] + ids = [] + for hf in hashfiles: + hf_id = hf.get("id") if isinstance(hf, dict) else hf + if hf_id is not None: + ids.append(hf_id) + return ids except Exception: return [] diff --git a/tests/test_hashview.py b/tests/test_hashview.py index 86e2a63..5714413 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -196,6 +196,49 @@ class TestHashviewAPI: result = api.list_customers() assert result["customers"] == [{"id": 2, "name": "Beta"}] + def test_get_hashfile_details_md5_zero(self, api): + """hash_type 0 (MD5) is falsy; must not fall through to the envelope `type`.""" + mock_resp = Mock() + mock_resp.json.return_value = { + "hash_type": 0, + "msg": "OK", + "status": 200, + "type": "message", + } + mock_resp.raise_for_status = Mock() + api.session.get.return_value = mock_resp + + details = api.get_hashfile_details(42) + assert details["hashtype"] == 0 + + def test_get_hashfile_details_ntlm(self, api): + """Sanity: NTLM (1000) still parses from `hash_type`.""" + mock_resp = Mock() + mock_resp.json.return_value = { + "hash_type": 1000, + "msg": "OK", + "status": 200, + "type": "message", + } + mock_resp.raise_for_status = Mock() + api.session.get.return_value = mock_resp + + assert api.get_hashfile_details(7)["hashtype"] == 1000 + + def test_get_hashfile_hash_type_reads_hashfiles_key(self, api): + """Endpoint returns {hashfiles: [...]} objects; return their ids.""" + mock_resp = Mock() + mock_resp.json.return_value = { + "status": 200, + "type": "message", + "msg": "OK", + "hashfiles": [{"id": 3, "name": "a"}, {"id": 9, "name": "b"}], + } + mock_resp.raise_for_status = Mock() + api.session.get.return_value = mock_resp + + assert api.get_hashfile_hash_type(1000) == [3, 9] + def test_upload_cracked_hashes_success(self, api, tmp_path): """Test uploading cracked hashes with valid lines (real API if possible).""" hashview_url, hashview_api_key = self._get_hashview_config()