import hashlib import json import logging import sqlite3 from pathlib import Path from datetime import datetime, timezone class Database: def __init__(self, db_path): Path(db_path).parent.mkdir( parents=True, exist_ok=True ) self.conn = sqlite3.connect( db_path, check_same_thread=False ) self.conn.row_factory = sqlite3.Row # # Оптимизация SQLite # self.conn.execute( "PRAGMA journal_mode=WAL" ) self.conn.execute( "PRAGMA synchronous=NORMAL" ) self.conn.execute( "PRAGMA temp_store=MEMORY" ) self.conn.execute( "PRAGMA cache_size=-100000" ) # # Буфер наблюдений точек доступа # self.observation_buffer = [] self.observation_batch_size = 1000 self.create_tables() # ========================================================= # Создание таблиц # ========================================================= def create_tables(self): # # Сессии захвата # self.conn.execute(""" CREATE TABLE IF NOT EXISTS capture_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time TEXT NOT NULL, end_time TEXT, capture_file TEXT NOT NULL, session_hash TEXT, session_hash_version INTEGER ) """) # # Точки доступа # self.conn.execute(""" CREATE TABLE IF NOT EXISTS access_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, bssid TEXT NOT NULL UNIQUE, essid TEXT, encryption TEXT, cipher TEXT, akm TEXT, country TEXT, channel INTEGER, frequency INTEGER, vendor TEXT, first_seen TEXT, last_seen TEXT, times_seen INTEGER DEFAULT 1, last_rssi INTEGER, last_latitude REAL, last_longitude REAL, last_speed REAL, has_handshake INTEGER DEFAULT 0, has_pmkid INTEGER DEFAULT 0, is_cracked INTEGER DEFAULT 0 ) """) # # Все найденные handshake / PMKID # self.conn.execute(""" CREATE TABLE IF NOT EXISTS handshakes ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_point_id INTEGER NOT NULL, session_id INTEGER, type TEXT NOT NULL, hash22000 TEXT, message_pair TEXT, captured_at TEXT, latitude REAL, longitude REAL, speed REAL, FOREIGN KEY(access_point_id) REFERENCES access_points(id), FOREIGN KEY(session_id) REFERENCES capture_sessions(id) ) """) # # Найденные пароли после hashcat # self.conn.execute(""" CREATE TABLE IF NOT EXISTS credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_point_id INTEGER NOT NULL, handshake_id INTEGER, password TEXT NOT NULL, source TEXT, created_at TEXT, verified INTEGER DEFAULT 0, UNIQUE(access_point_id, handshake_id, password), FOREIGN KEY(access_point_id) REFERENCES access_points(id), FOREIGN KEY(handshake_id) REFERENCES handshakes(id) ) """) # # GPS трек # self.conn.execute(""" CREATE TABLE IF NOT EXISTS gps_track ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER, sequence_no INTEGER NOT NULL, latitude REAL, longitude REAL, speed REAL, timestamp TEXT, FOREIGN KEY(session_id) REFERENCES capture_sessions(id) ) """) # # История наблюдений точек доступа # self.conn.execute(""" CREATE TABLE IF NOT EXISTS access_point_observations ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_point_id INTEGER NOT NULL, session_id INTEGER NOT NULL, observed_at TEXT NOT NULL, latitude REAL, longitude REAL, speed REAL, rssi INTEGER, channel INTEGER, frequency INTEGER, essid TEXT, encryption TEXT, cipher TEXT, akm TEXT, country TEXT, FOREIGN KEY(access_point_id) REFERENCES access_points(id), FOREIGN KEY(session_id) REFERENCES capture_sessions(id) ) """) # # Индексы handshakes # self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_handshakes_hash ON handshakes( hash22000 ) """) self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_handshakes_access_point ON handshakes( access_point_id ) """) # # Индексы истории наблюдений # self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_observation_access_point ON access_point_observations( access_point_id ) """) self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_observation_session ON access_point_observations( session_id ) """) self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_observation_time ON access_point_observations( observed_at ) """) self.conn.execute(""" CREATE INDEX IF NOT EXISTS idx_observation_ap_time ON access_point_observations( access_point_id, observed_at ) """) self._migrate_credentials_table() self._migrate_gps_track() self._migrate_session_hash() self.conn.commit() def _migrate_credentials_table( self ): columns = self.conn.execute( """ PRAGMA table_info(credentials) """ ).fetchall() column_names = { column[1] for column in columns } unique_indexes = self.conn.execute( """ PRAGMA index_list(credentials) """ ).fetchall() has_provenance_unique = False for index in unique_indexes: if not index[2]: continue index_name = index[1] index_columns = self.conn.execute( f""" PRAGMA index_info("{index_name}") """ ).fetchall() names = [ column[2] for column in index_columns ] if names == [ "access_point_id", "handshake_id", "password", ]: has_provenance_unique = True break if ( "handshake_id" in column_names and has_provenance_unique ): return # # Старая таблица credentials. # Пересоздаем её с актуальной схемой. # self.conn.execute( """ ALTER TABLE credentials RENAME TO credentials_old """ ) self.conn.execute(""" CREATE TABLE credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_point_id INTEGER NOT NULL, handshake_id INTEGER, password TEXT NOT NULL, source TEXT, created_at TEXT, verified INTEGER DEFAULT 0, UNIQUE(access_point_id, handshake_id, password), FOREIGN KEY(access_point_id) REFERENCES access_points(id), FOREIGN KEY(handshake_id) REFERENCES handshakes(id) ) """) if "handshake_id" in column_names: self.conn.execute( """ INSERT INTO credentials ( id, access_point_id, handshake_id, password, source, created_at, verified ) SELECT id, access_point_id, handshake_id, password, source, created_at, verified FROM credentials_old """ ) else: self.conn.execute( """ INSERT INTO credentials ( id, access_point_id, password, source, created_at, verified ) SELECT id, access_point_id, password, source, created_at, verified FROM credentials_old """ ) self.conn.execute( """ DROP TABLE credentials_old """ ) def _migrate_session_hash( self ): columns = self.conn.execute( """ PRAGMA table_info(capture_sessions) """ ).fetchall() column_names = { column[1] for column in columns } if "session_hash" not in column_names: self.conn.execute( """ ALTER TABLE capture_sessions ADD COLUMN session_hash TEXT """ ) if "session_hash_version" not in column_names: self.conn.execute( """ ALTER TABLE capture_sessions ADD COLUMN session_hash_version INTEGER """ ) self.conn.commit() self.conn.execute( "BEGIN" ) try: sessions = self.conn.execute( """ SELECT id FROM capture_sessions WHERE session_hash IS NULL OR session_hash_version IS NULL ORDER BY id """ ).fetchall() for session in sessions: session_hash = self._calculate_session_hash( session["id"] ) self.conn.execute( """ UPDATE capture_sessions SET session_hash=?, session_hash_version=1 WHERE id=? """, ( session_hash, session["id"] ) ) self.conn.commit() except Exception: self.conn.rollback() raise def _migrate_gps_track( self ): columns = self.conn.execute( """ PRAGMA table_info(gps_track) """ ).fetchall() column_names = { column[1] for column in columns } if "sequence_no" in column_names: return self.conn.execute( """ ALTER TABLE gps_track ADD COLUMN sequence_no INTEGER """ ) sessions = self.conn.execute( """ SELECT DISTINCT session_id FROM gps_track WHERE session_id IS NOT NULL ORDER BY session_id """ ).fetchall() for session in sessions: rows = self.conn.execute( """ SELECT id FROM gps_track WHERE session_id=? ORDER BY id """, ( session["session_id"], ) ).fetchall() for sequence_no, row in enumerate(rows): self.conn.execute( """ UPDATE gps_track SET sequence_no=? WHERE id=? """, ( sequence_no, row["id"] ) ) self.conn.commit() def _canonical_json( self, value ): return json.dumps( value, ensure_ascii=False, separators=(",", ":"), allow_nan=False ) def _calculate_session_hash( self, session_id ): session = self.conn.execute( """ SELECT start_time, end_time FROM capture_sessions WHERE id=? """, (session_id,) ).fetchone() if session is None: raise ValueError( f"Capture session not found: {session_id}" ) gps_rows = self.conn.execute( """ SELECT timestamp, latitude, longitude FROM gps_track WHERE session_id=? ORDER BY sequence_no """, (session_id,) ).fetchall() handshakes = self.conn.execute( """ SELECT access_points.bssid, handshakes.type, handshakes.hash22000, handshakes.message_pair, handshakes.captured_at FROM handshakes JOIN access_points ON access_points.id=handshakes.access_point_id WHERE handshakes.session_id=? ORDER BY access_points.bssid, handshakes.type, handshakes.hash22000, handshakes.message_pair, handshakes.captured_at """, (session_id,) ).fetchall() handshake_data = [ [ row["bssid"], row["type"], row["hash22000"], row["message_pair"], row["captured_at"] ] for row in handshakes ] if gps_rows: gps_data = [ [ row["timestamp"], row["latitude"], row["longitude"] ] for row in gps_rows ] canonical = [ "GPS_SESSION_V1", gps_data, handshake_data ] else: observation_rows = self.conn.execute( """ SELECT observations.observed_at, access_points.bssid FROM access_point_observations AS observations JOIN access_points ON access_points.id=observations.access_point_id WHERE observations.session_id=? ORDER BY observations.observed_at, access_points.bssid """, (session_id,) ).fetchall() observation_data = [ [ row["observed_at"], row["bssid"] ] for row in observation_rows ] canonical = [ "NON_GPS_SESSION_V1", session["start_time"], session["end_time"], observation_data, handshake_data ] payload = self._canonical_json( canonical ).encode( "utf-8" ) return hashlib.sha256( payload ).hexdigest() # ========================================================= # Capture sessions # ========================================================= def create_session( self, capture_file ): cursor = self.conn.execute( """ INSERT INTO capture_sessions ( start_time, capture_file ) VALUES (?,?) """, ( datetime.now( timezone.utc ).isoformat().replace( "+00:00", "Z" ), capture_file ) ) return cursor.lastrowid def begin( self ): self.conn.execute( "BEGIN" ) def commit( self ): self.conn.commit() def rollback( self ): self.conn.rollback() def close_session( self, session_id ): self.conn.execute( """ UPDATE capture_sessions SET end_time=? WHERE id=? """, ( datetime.now( timezone.utc ).isoformat().replace( "+00:00", "Z" ), session_id ) ) def finalize_session( self, session_id ): gps_count = self.conn.execute( """ SELECT COUNT(*) FROM gps_track WHERE session_id=? """, (session_id,) ).fetchone()[0] observation_count = self.conn.execute( """ SELECT COUNT(*) FROM access_point_observations WHERE session_id=? """, (session_id,) ).fetchone()[0] handshake_count = self.conn.execute( """ SELECT COUNT(*) FROM handshakes WHERE session_id=? """, (session_id,) ).fetchone()[0] if ( gps_count == 0 and observation_count == 0 and handshake_count == 0 ): self.conn.execute( """ DELETE FROM capture_sessions WHERE id=? """, (session_id,) ) logging.getLogger("project-wifi").info( "Session %s marked for removal during finalization: " "no GPS points, no AP observations, no handshakes/PMKID", session_id ) return False self.close_session(session_id) session_hash = self._calculate_session_hash( session_id ) self.conn.execute( """ UPDATE capture_sessions SET session_hash=?, session_hash_version=1 WHERE id=? """, ( session_hash, session_id ) ) logging.getLogger("project-wifi").info( "Session hash created: session_id=%s, version=1, hash=%s", session_id, session_hash ) return True # ========================================================= # Access points # ========================================================= def get_or_create_access_point( self, data ): bssid = data["bssid"] cursor = self.conn.execute( """ SELECT id FROM access_points WHERE bssid=? """, ( bssid, ) ) row = cursor.fetchone() now = data.get( "timestamp" ) if hasattr(now, "isoformat"): now = now.isoformat() if row: ap_id = row["id"] current = self.conn.execute( """ SELECT * FROM access_points WHERE id=? """, ( ap_id, ) ).fetchone() essid = data.get("essid") or current["essid"] encryption = ( data.get("encryption") or current["encryption"] ) cipher = ( data.get("cipher") or current["cipher"] ) akm = ( data.get("akm") or current["akm"] ) country = ( data.get("country") or current["country"] ) channel = ( data.get("channel") or current["channel"] ) frequency = ( data.get("frequency") or current["frequency"] ) vendor = ( data.get("vendor") or current["vendor"] ) incoming_latitude = data.get( "latitude" ) incoming_longitude = data.get( "longitude" ) current_latitude = current["last_latitude"] current_longitude = current["last_longitude"] current_has_coordinates = ( current_latitude is not None and current_longitude is not None and current_latitude != 0 and current_longitude != 0 ) incoming_has_coordinates = ( incoming_latitude is not None and incoming_longitude is not None and incoming_latitude != 0 and incoming_longitude != 0 ) if ( not current_has_coordinates and incoming_has_coordinates ): aggregate_latitude = incoming_latitude aggregate_longitude = incoming_longitude else: aggregate_latitude = current_latitude aggregate_longitude = current_longitude changed = ( essid != current["essid"] or encryption != current["encryption"] or cipher != current["cipher"] or akm != current["akm"] or country != current["country"] or channel != current["channel"] or frequency != current["frequency"] or vendor != current["vendor"] or data.get("rssi") != current["last_rssi"] or aggregate_latitude != current["last_latitude"] or aggregate_longitude != current["last_longitude"] or data.get("speed") != current["last_speed"] ) if changed: self.conn.execute( """ UPDATE access_points SET essid=?, encryption=?, cipher=?, akm=?, country=?, channel=?, frequency=?, vendor=?, last_seen=?, times_seen=times_seen+1, last_rssi=?, last_latitude=?, last_longitude=?, last_speed=? WHERE id=? """, ( essid, encryption, cipher, akm, country, channel, frequency, vendor, now, data.get("rssi"), aggregate_latitude, aggregate_longitude, data.get("speed"), ap_id ) ) else: self.conn.execute( """ UPDATE access_points SET last_seen=?, times_seen=times_seen+1 WHERE id=? """, ( now, ap_id ) ) else: cursor = self.conn.execute( """ INSERT INTO access_points ( bssid, essid, encryption, cipher, akm, country, channel, frequency, vendor, first_seen, last_seen, last_rssi, last_latitude, last_longitude, last_speed ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( data.get("bssid"), data.get("essid"), data.get("encryption"), data.get("cipher"), data.get("akm"), data.get("country"), data.get("channel"), data.get("frequency"), data.get("vendor"), now, now, data.get("rssi"), data.get("latitude"), data.get("longitude"), data.get("speed") ) ) ap_id = cursor.lastrowid return ap_id def get_access_point_by_bssid( self, bssid ): cursor = self.conn.execute( """ SELECT id, essid, encryption, cipher, akm, country, channel, frequency, vendor FROM access_points WHERE bssid=? """, ( bssid, ) ) row = cursor.fetchone() return row def handshake_exists( self, access_point_id, session_id, hash22000 ): cursor = self.conn.execute( """ SELECT id FROM handshakes WHERE access_point_id=? AND session_id=? AND hash22000=? LIMIT 1 """, ( access_point_id, session_id, hash22000 ) ) return cursor.fetchone() is not None # ========================================================= # Handshakes / PMKID # ========================================================= def add_handshake( self, data ): try: self.conn.execute( """ INSERT INTO handshakes ( access_point_id, session_id, type, hash22000, message_pair, captured_at, latitude, longitude, speed ) VALUES (?,?,?,?,?,?,?,?,?) """, ( data["access_point_id"], data.get("session_id"), data["type"], data.get("hash22000"), data.get("message_pair"), data.get("captured_at"), data.get("latitude"), data.get("longitude"), data.get("speed") ) ) if data["type"] == "PMKID": self.conn.execute( """ UPDATE access_points SET has_pmkid=1 WHERE id=? """, ( data["access_point_id"], ) ) if data["type"] == "EAPOL": self.conn.execute( """ UPDATE access_points SET has_handshake=1 WHERE id=? """, ( data["access_point_id"], ) ) return True except sqlite3.IntegrityError: return False # ========================================================= # Credentials / hashcat # ========================================================= def add_credential( self, access_point_id, password, source="hashcat" ): self.conn.execute( """ INSERT OR IGNORE INTO credentials ( access_point_id, password, source, created_at ) VALUES (?,?,?,?) """, ( access_point_id, password, source, datetime.now( timezone.utc ).isoformat().replace( "+00:00", "Z" ) ) ) self.conn.execute( """ UPDATE access_points SET is_cracked=1 WHERE id=? """, ( access_point_id, ) ) def credential_exists( self, access_point_id, password ): cursor = self.conn.execute( """ SELECT id FROM credentials WHERE access_point_id=? AND password=? """, ( access_point_id, password ) ) return cursor.fetchone() is not None # ========================================================= # Hashcat # ========================================================= def add_credential_by_hash( self, hash22000, password ): access_point_id = self.get_access_point_by_hash( hash22000 ) if access_point_id is None: return False if self.credential_exists( access_point_id, password ): return False self.add_credential( access_point_id=access_point_id, password=password, source="hashcat" ) return True # ========================================================= # Access point observations # ========================================================= def add_observation( self, access_point_id, session_id, data ): """ Добавляет одно наблюдение точки доступа. Каждая запись соответствует одному обнаружению точки доступа во время обработки access_points.csv. """ observed_at = data.get( "timestamp" ) if hasattr( observed_at, "isoformat" ): observed_at = observed_at.isoformat() self.observation_buffer.append( ( access_point_id, session_id, observed_at, data.get( "latitude" ), data.get( "longitude" ), data.get( "speed" ), data.get( "rssi" ), data.get( "channel" ), data.get( "frequency" ), data.get( "essid" ), data.get( "encryption" ), data.get( "cipher" ), data.get( "akm" ), data.get( "country" ) ) ) if ( len( self.observation_buffer ) >= self.observation_batch_size ): self.flush_observations() def flush_observations( self ): if not self.observation_buffer: return self.conn.executemany( """ INSERT INTO access_point_observations ( access_point_id, session_id, observed_at, latitude, longitude, speed, rssi, channel, frequency, essid, encryption, cipher, akm, country ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) """, self.observation_buffer ) self.observation_buffer.clear() # ========================================================= # Полная регистрация наблюдения точки доступа # ========================================================= def record_access_point_observation( self, session_id, data ): """ Полностью обрабатывает одно наблюдение точки доступа. Метод: • создает новую точку доступа при необходимости; • обновляет агрегированную карточку; • сохраняет историю наблюдений. """ access_point_id = ( self.get_or_create_access_point( data ) ) self.add_observation( access_point_id=access_point_id, session_id=session_id, data=data ) if ( len( self.observation_buffer ) >= self.observation_batch_size ): self.flush_observations() return access_point_id # ========================================================= # GPS # ========================================================= def add_gps_point( self, session_id, gps ): sequence_no = self.conn.execute( """ SELECT COALESCE( MAX(sequence_no), -1 ) + 1 FROM gps_track WHERE session_id=? """, ( session_id, ) ).fetchone()[0] self.conn.execute( """ INSERT INTO gps_track ( session_id, sequence_no, latitude, longitude, speed, timestamp ) VALUES (?,?,?,?,?,?) """, ( session_id, sequence_no, gps.latitude, gps.longitude, gps.speed, gps.timestamp.isoformat().replace( "+00:00", "Z" ) ) ) # ========================================================= # Close # ========================================================= def close(self): self.flush_observations() self.conn.commit() self.conn.close()