#!/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