Files
wifi-gps-mapper-public/src/database.py
T
2026-10-06 21:08:12 +03:00

1973 lines
37 KiB
Python

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