1973 lines
37 KiB
Python
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() |