Files
2026-10-06 21:08:12 +03:00

834 lines
16 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Общая бизнес-логика Hashcat.
Этот модуль используется одновременно:
• серверным Reports;
• CLI tools/hashcat.py.
Модуль не открывает соединение с SQLite самостоятельно.
Соединение передаётся вызывающим кодом и обслуживается
существующим reports.database.
"""
from datetime import datetime, timezone
from pathlib import Path
import re
# ============================================================
# Constants
# ============================================================
MAC_PATTERN = re.compile(
r"^[0-9a-fA-F]{12}$"
)
SUPPORTED_HANDSHAKE_TYPES = {
"WPA*01",
"WPA*02",
}
# ============================================================
# Export filename
# ============================================================
def sanitize_export_query(
query,
):
value = str(
query or ""
).strip().lower()
value = re.sub(
r"[^a-z0-9]+",
"-",
value
)
value = value.strip(
"-"
)
if not value:
value = "all"
return value[
:80
]
def build_export_filename(
export_type,
query="",
):
timestamp = datetime.now(
timezone.utc
).strftime(
"%Y%m%d-%H%M%S"
)
if export_type == "all":
return (
"wifi-gps-mapper-all-"
f"{timestamp}.hc22000"
)
if export_type == "search":
safe_query = sanitize_export_query(
query
)
return (
"wifi-gps-mapper-search-"
f"{safe_query}-"
f"{timestamp}.hc22000"
)
raise ValueError(
f"Unknown export type: {export_type}"
)
# ============================================================
# Hash export
# ============================================================
def _build_hash_export_from_rows(
rows,
):
hashes = [
str(
row["hash22000"]
)
for row in rows
if row["hash22000"] is not None
and str(
row["hash22000"]
).strip()
]
if not hashes:
return ""
return (
"\n".join(
hashes
)
+
"\n"
)
def get_all_hashes(
conn,
):
cursor = conn.execute(
"""
SELECT DISTINCT
h.hash22000
FROM handshakes AS h
WHERE
h.hash22000 IS NOT NULL
AND TRIM(h.hash22000) != ''
AND NOT EXISTS (
SELECT 1
FROM credentials AS c
WHERE c.handshake_id = h.id
)
ORDER BY
h.hash22000
"""
)
return cursor.fetchall()
def build_all_hash_export(
conn,
):
rows = get_all_hashes(
conn
)
return _build_hash_export_from_rows(
rows
)
# ============================================================
# Database statistics
# ============================================================
def get_export_statistics(
conn,
):
rows = get_all_hashes(
conn
)
statistics = {
"exported_hashes": len(rows),
"wpa01": 0,
"wpa02": 0,
}
for row in rows:
hash_value = str(
row["hash22000"]
)
if hash_value.startswith(
"WPA*01*"
):
statistics["wpa01"] += 1
elif hash_value.startswith(
"WPA*02*"
):
statistics["wpa02"] += 1
return statistics
def get_database_statistics(
conn,
):
statistics = {}
cursor = conn.execute(
"""
SELECT COUNT(*)
FROM access_points
"""
)
statistics["access_points"] = cursor.fetchone()[0]
cursor = conn.execute(
"""
SELECT COUNT(*)
FROM handshakes
"""
)
statistics["handshakes"] = cursor.fetchone()[0]
cursor = conn.execute(
"""
SELECT COUNT(*)
FROM handshakes
WHERE
hash22000 IS NOT NULL
AND TRIM(hash22000) != ''
"""
)
statistics["hashes"] = cursor.fetchone()[0]
cursor = conn.execute(
"""
SELECT COUNT(DISTINCT hash22000)
FROM handshakes
WHERE
hash22000 IS NOT NULL
AND TRIM(hash22000) != ''
"""
)
statistics["unique_hashes"] = cursor.fetchone()[0]
cursor = conn.execute(
"""
SELECT COUNT(*)
FROM credentials
"""
)
statistics["credentials"] = cursor.fetchone()[0]
cursor = conn.execute(
"""
SELECT COUNT(DISTINCT c.access_point_id)
FROM credentials AS c
JOIN handshakes AS h
ON h.id = c.handshake_id
"""
)
statistics["cracked_access_points"] = cursor.fetchone()[0]
return statistics
# ============================================================
# Hashcat --show parser
# ============================================================
def _normalize_mac(
value,
):
value = str(
value or ""
).strip().lower()
value = value.replace(
":",
""
)
value = value.replace(
"-",
""
)
return value
def _normalize_hashcat_essid(
value,
):
value = str(
value or ""
).strip()
if (
value.startswith("$HEX[")
and value.endswith("]")
):
encoded = value[5:-1]
if not encoded:
return ""
if (
len(encoded) % 2 != 0
or re.fullmatch(
r"[0-9a-fA-F]+",
encoded
) is None
):
raise ValueError(
"ESSID $HEX value must be hexadecimal."
)
return bytes.fromhex(
encoded
).hex()
return value.encode(
"utf-8"
).hex()
def _parse_handshake_hash(
value,
):
value = str(
value or ""
).strip()
if not value:
raise ValueError(
"HASH is empty."
)
parts = value.split(
"*"
)
if len(parts) < 6:
raise ValueError(
"HASH is not a valid WPA hash."
)
handshake_type = (
f"{parts[0]}*{parts[1]}"
)
if handshake_type not in SUPPORTED_HANDSHAKE_TYPES:
raise ValueError(
"Unsupported handshake type: "
f"{handshake_type}."
)
hash_value = parts[2].strip().lower()
ap_bssid = _normalize_mac(
parts[3]
)
client_mac = _normalize_mac(
parts[4]
)
essid = parts[5].strip().lower()
if not hash_value:
raise ValueError(
"HASH value is empty."
)
if (
len(hash_value) != 32
or re.fullmatch(
r"[0-9a-f]{32}",
hash_value
) is None
):
raise ValueError(
"HASH value must be 32 hexadecimal characters."
)
if not MAC_PATTERN.fullmatch(
ap_bssid
):
raise ValueError(
"AP_BSSID is not a valid MAC address."
)
if not MAC_PATTERN.fullmatch(
client_mac
):
raise ValueError(
"CLIENT_MAC is not a valid MAC address."
)
if essid and (
len(essid) % 2 != 0
or re.fullmatch(
r"[0-9a-fA-F]+",
essid
) is None
):
raise ValueError(
"ESSID must be hexadecimal."
)
return {
"handshake_type": handshake_type,
"hash": hash_value.lower(),
"ap_bssid": ap_bssid,
"client_mac": client_mac,
"essid": essid,
}
def parse_hashcat_show_line(
line,
line_number,
):
raw = str(
line
).rstrip(
"\r\n"
)
if not raw.strip():
raise ValueError(
"Empty line."
)
parts = raw.split(
":",
4
)
if len(parts) != 5:
raise ValueError(
"Expected format: "
"HASH:AP_BSSID:CLIENT_MAC:ESSID:PASSWORD."
)
hash_value = parts[0].strip().lower()
ap_bssid = _normalize_mac(
parts[1]
)
client_mac = _normalize_mac(
parts[2]
)
essid = parts[3].strip()
password = parts[4]
if not hash_value:
raise ValueError(
"HASH is empty."
)
if (
len(hash_value) != 32
or re.fullmatch(
r"[0-9a-f]{32}",
hash_value
) is None
):
raise ValueError(
"HASH value must be 32 hexadecimal characters."
)
if not MAC_PATTERN.fullmatch(
ap_bssid
):
raise ValueError(
"AP_BSSID is not a valid MAC address."
)
if not MAC_PATTERN.fullmatch(
client_mac
):
raise ValueError(
"CLIENT_MAC is not a valid MAC address."
)
essid_hex = _normalize_hashcat_essid(
essid
)
return {
"line_number": line_number,
"hash22000": hash_value,
"hash": hash_value,
"ap_bssid": ap_bssid,
"client_mac": client_mac,
"essid": essid_hex,
"password": password,
}
# ============================================================
# Handshake matching
# ============================================================
def find_matching_handshake(
conn,
parsed,
):
from reports.database import query_all
rows = query_all(
conn,
"""
SELECT
h.id,
h.access_point_id,
h.hash22000
FROM handshakes AS h
WHERE
h.hash22000 IS NOT NULL
AND TRIM(h.hash22000) != ''
ORDER BY
h.id
"""
)
for row in rows:
try:
handshake = _parse_handshake_hash(
row["hash22000"]
)
except ValueError:
continue
if (
handshake["hash"]
== parsed["hash"]
and handshake["ap_bssid"]
== parsed["ap_bssid"]
and handshake["client_mac"]
== parsed["client_mac"]
):
return row
return None
# ============================================================
# Credential operations
# ============================================================
def credential_exists(
conn,
access_point_id,
handshake_id,
password,
):
from reports.database import query_one
row = query_one(
conn,
"""
SELECT id
FROM credentials
WHERE
access_point_id = ?
AND handshake_id = ?
AND password = ?
LIMIT 1
""",
(
access_point_id,
handshake_id,
password,
)
)
return row is not None
def add_credential(
conn,
access_point_id,
handshake_id,
password,
):
from reports.database import execute
now = datetime.now(
timezone.utc
).isoformat().replace(
"+00:00",
"Z"
)
cursor = execute(
conn,
"""
INSERT INTO credentials
(
access_point_id,
handshake_id,
password,
source,
created_at
)
VALUES
(
?,
?,
?,
?,
?
)
""",
(
access_point_id,
handshake_id,
password,
"hashcat",
now,
)
)
execute(
conn,
"""
UPDATE access_points
SET
is_cracked = 1
WHERE
id = ?
""",
(
access_point_id,
)
)
return cursor.lastrowid
# ============================================================
# Import validation
# ============================================================
def validate_hashcat_show_file(
conn,
lines,
):
statistics = {
"total_lines": 0,
"valid_lines": 0,
"invalid_lines": 0,
"matched_handshakes": 0,
"missing_handshakes": 0,
"new_credentials": 0,
"duplicate_credentials": 0,
"already_existing": 0,
"errors": [],
"missing": [],
"items": [],
}
seen = set()
seen_credentials = set()
for line_number, line in enumerate(
lines,
start=1
):
statistics["total_lines"] += 1
try:
parsed = parse_hashcat_show_line(
line,
line_number
)
key = (
parsed["hash"],
parsed["ap_bssid"],
parsed["client_mac"],
parsed["essid"],
parsed["password"],
)
if key in seen:
raise ValueError(
"Duplicate line in input file."
)
seen.add(
key
)
handshake = find_matching_handshake(
conn,
parsed
)
if handshake is None:
statistics["missing_handshakes"] += 1
statistics["missing"].append(
{
"line": line_number,
"hash":
parsed["hash"],
"ap_bssid":
parsed["ap_bssid"],
"client_mac":
parsed["client_mac"],
"essid":
parsed["essid"],
"password":
parsed["password"],
}
)
continue
statistics["matched_handshakes"] += 1
credential_key = (
handshake["access_point_id"],
handshake["id"],
parsed["password"],
)
if credential_key in seen_credentials:
statistics["duplicate_credentials"] += 1
status = "Duplicate credential"
else:
seen_credentials.add(
credential_key
)
exists = credential_exists(
conn,
handshake["access_point_id"],
handshake["id"],
parsed["password"]
)
if exists:
statistics["already_existing"] += 1
status = "Already exists"
else:
statistics["new_credentials"] += 1
status = "New credential"
statistics["valid_lines"] += 1
statistics["items"].append(
{
"line": line_number,
"access_point_id":
handshake["access_point_id"],
"handshake_id":
handshake["id"],
"password":
parsed["password"],
"status": status,
}
)
except ValueError as error:
statistics["invalid_lines"] += 1
statistics["errors"].append(
{
"line": line_number,
"reason": str(
error
),
}
)
return statistics
# ============================================================
# Atomic import
# ============================================================
def import_hashcat_show(
conn,
lines,
):
lines = list(
lines
)
validation = validate_hashcat_show_file(
conn,
lines
)
if validation["errors"]:
return {
**validation,
"committed": False,
"result": "Rejected",
}
try:
for item in validation["items"]:
if item["status"] != "New credential":
continue
add_credential(
conn,
item["access_point_id"],
item["handshake_id"],
item["password"]
)
conn.commit()
return {
**validation,
"committed": True,
"result": "Imported",
}
except Exception:
conn.rollback()
raise