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()