2112 lines
52 KiB
Python
2112 lines
52 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Общий сервис импорта и экспорта capture sessions в формате .wifitrip.
|
|
|
|
Модуль не зависит от Flask и scanner.
|
|
CLI и Web UI используют один и тот же сервисный код.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import tempfile
|
|
import time
|
|
import zipfile
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
WIFITRIP_FORMAT = "wifitrip"
|
|
WIFITRIP_VERSION = 1
|
|
SESSION_HASH_VERSION = 1
|
|
|
|
REQUIRED_FILES = (
|
|
"manifest.json",
|
|
"sessions.json",
|
|
"gps_tracks.json",
|
|
"access_points.json",
|
|
"observations.json",
|
|
"handshakes.json",
|
|
"credentials.json",
|
|
)
|
|
|
|
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
|
|
class WifitripError(ValueError):
|
|
"""Ошибка формата или обработки .wifitrip."""
|
|
|
|
|
|
def _json_bytes(value):
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
|
|
def _json_loads(data, filename):
|
|
try:
|
|
return json.loads(
|
|
data.decode("utf-8"),
|
|
parse_constant=_reject_non_finite,
|
|
)
|
|
except UnicodeDecodeError as exc:
|
|
raise WifitripError(
|
|
f"{filename} must be valid UTF-8."
|
|
) from exc
|
|
except json.JSONDecodeError as exc:
|
|
raise WifitripError(
|
|
f"{filename} contains invalid JSON: {exc}"
|
|
) from exc
|
|
|
|
|
|
def _reject_non_finite(value):
|
|
raise ValueError(
|
|
f"non-finite JSON number is not allowed: {value}"
|
|
)
|
|
|
|
|
|
def _require_dict(value, name):
|
|
if not isinstance(value, dict):
|
|
raise WifitripError(
|
|
f"{name} must contain a JSON object."
|
|
)
|
|
|
|
|
|
def _require_list(value, name):
|
|
if not isinstance(value, list):
|
|
raise WifitripError(
|
|
f"{name} must contain a JSON array."
|
|
)
|
|
|
|
|
|
def _require_string(value, field, allow_none=False):
|
|
if value is None and allow_none:
|
|
return
|
|
|
|
if not isinstance(value, str):
|
|
raise WifitripError(
|
|
f"{field} must be a string."
|
|
)
|
|
|
|
|
|
def _require_number(value, field, allow_none=True):
|
|
if value is None and allow_none:
|
|
return
|
|
|
|
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
raise WifitripError(
|
|
f"{field} must be a number or null."
|
|
)
|
|
|
|
|
|
def _require_integer(value, field, allow_none=False):
|
|
if value is None and allow_none:
|
|
return
|
|
|
|
if not isinstance(value, int) or isinstance(value, bool):
|
|
raise WifitripError(
|
|
f"{field} must be an integer."
|
|
)
|
|
|
|
|
|
def _require_bssid(value, field):
|
|
_require_string(value, field)
|
|
|
|
if not value.strip():
|
|
raise WifitripError(
|
|
f"{field} must not be empty."
|
|
)
|
|
|
|
|
|
def _validate_hash(value, field):
|
|
_require_string(value, field)
|
|
|
|
if not SHA256_RE.fullmatch(value):
|
|
raise WifitripError(
|
|
f"{field} must be a 64-character lowercase SHA-256 hash."
|
|
)
|
|
|
|
|
|
def _read_package(path):
|
|
path = Path(path)
|
|
|
|
if not path.exists():
|
|
raise WifitripError(
|
|
f"File does not exist: {path}"
|
|
)
|
|
|
|
if not path.is_file():
|
|
raise WifitripError(
|
|
f"Not a regular file: {path}"
|
|
)
|
|
|
|
try:
|
|
archive = zipfile.ZipFile(path, "r")
|
|
except (OSError, zipfile.BadZipFile) as exc:
|
|
raise WifitripError(
|
|
f"Invalid .wifitrip ZIP archive: {path}"
|
|
) from exc
|
|
|
|
with archive:
|
|
infos = archive.infolist()
|
|
|
|
names = [info.filename for info in infos]
|
|
|
|
if len(names) != len(set(names)):
|
|
raise WifitripError(
|
|
"Archive contains duplicate file names."
|
|
)
|
|
|
|
missing = [
|
|
name
|
|
for name in REQUIRED_FILES
|
|
if name not in names
|
|
]
|
|
|
|
if missing:
|
|
raise WifitripError(
|
|
"Archive is missing required files: "
|
|
+ ", ".join(missing)
|
|
)
|
|
|
|
data = {}
|
|
|
|
for filename in REQUIRED_FILES:
|
|
try:
|
|
raw = archive.read(filename)
|
|
except KeyError as exc:
|
|
raise WifitripError(
|
|
f"Unable to read {filename}."
|
|
) from exc
|
|
|
|
data[filename] = _json_loads(
|
|
raw,
|
|
filename,
|
|
)
|
|
|
|
_validate_package(data)
|
|
|
|
return data
|
|
|
|
|
|
def _validate_package(data):
|
|
for filename in REQUIRED_FILES:
|
|
if filename not in data:
|
|
raise WifitripError(
|
|
f"Missing package member: {filename}"
|
|
)
|
|
|
|
manifest = data["manifest.json"]
|
|
sessions = data["sessions.json"]
|
|
gps_tracks = data["gps_tracks.json"]
|
|
access_points = data["access_points.json"]
|
|
observations = data["observations.json"]
|
|
handshakes = data["handshakes.json"]
|
|
credentials = data["credentials.json"]
|
|
|
|
_require_dict(
|
|
manifest,
|
|
"manifest.json",
|
|
)
|
|
_require_list(
|
|
sessions,
|
|
"sessions.json",
|
|
)
|
|
_require_list(
|
|
gps_tracks,
|
|
"gps_tracks.json",
|
|
)
|
|
_require_list(
|
|
access_points,
|
|
"access_points.json",
|
|
)
|
|
_require_list(
|
|
observations,
|
|
"observations.json",
|
|
)
|
|
_require_list(
|
|
handshakes,
|
|
"handshakes.json",
|
|
)
|
|
_require_list(
|
|
credentials,
|
|
"credentials.json",
|
|
)
|
|
|
|
if manifest.get("format") != WIFITRIP_FORMAT:
|
|
raise WifitripError(
|
|
"Unsupported package format."
|
|
)
|
|
|
|
if manifest.get("format_version") != WIFITRIP_VERSION:
|
|
raise WifitripError(
|
|
"Unsupported .wifitrip format version: "
|
|
f"{manifest.get('format_version')}"
|
|
)
|
|
|
|
_require_string(
|
|
manifest.get("created_at"),
|
|
"manifest.created_at",
|
|
)
|
|
|
|
if manifest.get("sessions") != len(sessions):
|
|
raise WifitripError(
|
|
"manifest.sessions does not match sessions.json."
|
|
)
|
|
|
|
session_hash_versions = manifest.get(
|
|
"session_hash_versions"
|
|
)
|
|
|
|
if not isinstance(
|
|
session_hash_versions,
|
|
list,
|
|
):
|
|
raise WifitripError(
|
|
"manifest.session_hash_versions must be a list."
|
|
)
|
|
|
|
if not session_hash_versions:
|
|
raise WifitripError(
|
|
"manifest.session_hash_versions must not be empty."
|
|
)
|
|
|
|
if any(
|
|
version != SESSION_HASH_VERSION
|
|
for version in session_hash_versions
|
|
):
|
|
raise WifitripError(
|
|
"Package contains an unsupported session hash version."
|
|
)
|
|
|
|
session_map = {}
|
|
session_versions = {}
|
|
|
|
for index, session in enumerate(sessions):
|
|
_require_dict(
|
|
session,
|
|
f"sessions.json[{index}]",
|
|
)
|
|
|
|
session_hash = session.get(
|
|
"session_hash"
|
|
)
|
|
session_hash_version = session.get(
|
|
"session_hash_version"
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"sessions.json[{index}].session_hash",
|
|
)
|
|
|
|
_require_integer(
|
|
session_hash_version,
|
|
f"sessions.json[{index}].session_hash_version",
|
|
)
|
|
|
|
if session_hash_version != SESSION_HASH_VERSION:
|
|
raise WifitripError(
|
|
"Unsupported session hash version: "
|
|
f"{session_hash_version}"
|
|
)
|
|
|
|
key = (
|
|
session_hash,
|
|
session_hash_version,
|
|
)
|
|
|
|
if key in session_map:
|
|
raise WifitripError(
|
|
"Duplicate session hash in package: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
_require_string(
|
|
session.get("start_time"),
|
|
f"sessions.json[{index}].start_time",
|
|
)
|
|
|
|
_require_string(
|
|
session.get("end_time"),
|
|
f"sessions.json[{index}].end_time",
|
|
allow_none=True,
|
|
)
|
|
|
|
session_type = session.get(
|
|
"session_type"
|
|
)
|
|
|
|
if session_type is not None:
|
|
if session_type not in (
|
|
"gps",
|
|
"non_gps",
|
|
):
|
|
raise WifitripError(
|
|
"Invalid session_type in "
|
|
f"sessions.json[{index}]."
|
|
)
|
|
|
|
session_map[key] = session
|
|
session_versions[session_hash] = (
|
|
session_hash_version
|
|
)
|
|
|
|
ap_map = {}
|
|
|
|
for index, access_point in enumerate(
|
|
access_points
|
|
):
|
|
_require_dict(
|
|
access_point,
|
|
f"access_points.json[{index}]",
|
|
)
|
|
|
|
bssid = access_point.get(
|
|
"bssid"
|
|
)
|
|
|
|
_require_bssid(
|
|
bssid,
|
|
f"access_points.json[{index}].bssid",
|
|
)
|
|
|
|
if bssid in ap_map:
|
|
raise WifitripError(
|
|
"Duplicate BSSID in package: "
|
|
f"{bssid}"
|
|
)
|
|
|
|
ap_map[bssid] = True
|
|
|
|
gps_sequences = {}
|
|
|
|
for index, point in enumerate(
|
|
gps_tracks
|
|
):
|
|
_require_dict(
|
|
point,
|
|
f"gps_tracks.json[{index}]",
|
|
)
|
|
|
|
session_hash = point.get(
|
|
"session_hash"
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"gps_tracks.json[{index}].session_hash",
|
|
)
|
|
|
|
if session_hash not in session_versions:
|
|
raise WifitripError(
|
|
"GPS track references unknown session: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
sequence_no = point.get(
|
|
"sequence_no"
|
|
)
|
|
|
|
_require_integer(
|
|
sequence_no,
|
|
f"gps_tracks.json[{index}].sequence_no",
|
|
)
|
|
|
|
if sequence_no < 0:
|
|
raise WifitripError(
|
|
"GPS sequence_no must not be negative."
|
|
)
|
|
|
|
_require_string(
|
|
point.get("timestamp"),
|
|
f"gps_tracks.json[{index}].timestamp",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_number(
|
|
point.get("latitude"),
|
|
f"gps_tracks.json[{index}].latitude",
|
|
)
|
|
|
|
_require_number(
|
|
point.get("longitude"),
|
|
f"gps_tracks.json[{index}].longitude",
|
|
)
|
|
|
|
_require_number(
|
|
point.get("speed"),
|
|
f"gps_tracks.json[{index}].speed",
|
|
)
|
|
|
|
sequences = gps_sequences.setdefault(
|
|
session_hash,
|
|
set(),
|
|
)
|
|
|
|
if sequence_no in sequences:
|
|
raise WifitripError(
|
|
"Duplicate GPS sequence_no for session: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
sequences.add(sequence_no)
|
|
|
|
for session_hash, sequences in gps_sequences.items():
|
|
expected = set(
|
|
range(len(sequences))
|
|
)
|
|
|
|
if sequences != expected:
|
|
raise WifitripError(
|
|
"GPS sequence numbers must be contiguous "
|
|
f"starting at zero for session {session_hash}."
|
|
)
|
|
|
|
for index, observation in enumerate(
|
|
observations
|
|
):
|
|
_require_dict(
|
|
observation,
|
|
f"observations.json[{index}]",
|
|
)
|
|
|
|
session_hash = observation.get(
|
|
"session_hash"
|
|
)
|
|
bssid = observation.get(
|
|
"bssid"
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"observations.json[{index}].session_hash",
|
|
)
|
|
|
|
_require_bssid(
|
|
bssid,
|
|
f"observations.json[{index}].bssid",
|
|
)
|
|
|
|
if session_hash not in session_versions:
|
|
raise WifitripError(
|
|
"Observation references unknown session: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
if bssid not in ap_map:
|
|
raise WifitripError(
|
|
"Observation references unknown BSSID: "
|
|
f"{bssid}"
|
|
)
|
|
|
|
_require_string(
|
|
observation.get("observed_at"),
|
|
f"observations.json[{index}].observed_at",
|
|
)
|
|
|
|
_require_number(
|
|
observation.get("latitude"),
|
|
f"observations.json[{index}].latitude",
|
|
)
|
|
|
|
_require_number(
|
|
observation.get("longitude"),
|
|
f"observations.json[{index}].longitude",
|
|
)
|
|
|
|
_require_number(
|
|
observation.get("speed"),
|
|
f"observations.json[{index}].speed",
|
|
)
|
|
|
|
_require_integer(
|
|
observation.get("rssi"),
|
|
f"observations.json[{index}].rssi",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_integer(
|
|
observation.get("channel"),
|
|
f"observations.json[{index}].channel",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_integer(
|
|
observation.get("frequency"),
|
|
f"observations.json[{index}].frequency",
|
|
allow_none=True,
|
|
)
|
|
|
|
for field in (
|
|
"essid",
|
|
"encryption",
|
|
"cipher",
|
|
"akm",
|
|
"country",
|
|
):
|
|
_require_string(
|
|
observation.get(field),
|
|
f"observations.json[{index}].{field}",
|
|
allow_none=True,
|
|
)
|
|
|
|
handshake_keys = {}
|
|
|
|
for index, handshake in enumerate(
|
|
handshakes
|
|
):
|
|
_require_dict(
|
|
handshake,
|
|
f"handshakes.json[{index}]",
|
|
)
|
|
|
|
session_hash = handshake.get(
|
|
"session_hash"
|
|
)
|
|
bssid = handshake.get(
|
|
"bssid"
|
|
)
|
|
handshake_key = handshake.get(
|
|
"handshake_key"
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"handshakes.json[{index}].session_hash",
|
|
)
|
|
|
|
_require_bssid(
|
|
bssid,
|
|
f"handshakes.json[{index}].bssid",
|
|
)
|
|
|
|
_require_string(
|
|
handshake_key,
|
|
f"handshakes.json[{index}].handshake_key",
|
|
)
|
|
|
|
if session_hash not in session_versions:
|
|
raise WifitripError(
|
|
"Handshake references unknown session: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
if bssid not in ap_map:
|
|
raise WifitripError(
|
|
"Handshake references unknown BSSID: "
|
|
f"{bssid}"
|
|
)
|
|
|
|
session_keys = handshake_keys.setdefault(
|
|
session_hash,
|
|
set(),
|
|
)
|
|
|
|
if handshake_key in session_keys:
|
|
raise WifitripError(
|
|
"Duplicate handshake_key for session: "
|
|
f"{session_hash}: {handshake_key}"
|
|
)
|
|
|
|
session_keys.add(handshake_key)
|
|
|
|
_require_string(
|
|
handshake.get("type"),
|
|
f"handshakes.json[{index}].type",
|
|
)
|
|
|
|
_require_string(
|
|
handshake.get("hash22000"),
|
|
f"handshakes.json[{index}].hash22000",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_string(
|
|
handshake.get("message_pair"),
|
|
f"handshakes.json[{index}].message_pair",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_string(
|
|
handshake.get("captured_at"),
|
|
f"handshakes.json[{index}].captured_at",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_number(
|
|
handshake.get("latitude"),
|
|
f"handshakes.json[{index}].latitude",
|
|
)
|
|
|
|
_require_number(
|
|
handshake.get("longitude"),
|
|
f"handshakes.json[{index}].longitude",
|
|
)
|
|
|
|
_require_number(
|
|
handshake.get("speed"),
|
|
f"handshakes.json[{index}].speed",
|
|
)
|
|
|
|
for index, credential in enumerate(
|
|
credentials
|
|
):
|
|
_require_dict(
|
|
credential,
|
|
f"credentials.json[{index}]",
|
|
)
|
|
|
|
session_hash = credential.get(
|
|
"session_hash"
|
|
)
|
|
bssid = credential.get(
|
|
"bssid"
|
|
)
|
|
handshake_key = credential.get(
|
|
"handshake_key"
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"credentials.json[{index}].session_hash",
|
|
)
|
|
|
|
_require_bssid(
|
|
bssid,
|
|
f"credentials.json[{index}].bssid",
|
|
)
|
|
|
|
if session_hash not in session_versions:
|
|
raise WifitripError(
|
|
"Credential references unknown session: "
|
|
f"{session_hash}"
|
|
)
|
|
|
|
if bssid not in ap_map:
|
|
raise WifitripError(
|
|
"Credential references unknown BSSID: "
|
|
f"{bssid}"
|
|
)
|
|
|
|
if handshake_key is not None:
|
|
_require_string(
|
|
handshake_key,
|
|
f"credentials.json[{index}].handshake_key",
|
|
)
|
|
|
|
if handshake_key not in handshake_keys.get(
|
|
session_hash,
|
|
set(),
|
|
):
|
|
raise WifitripError(
|
|
"Credential references unknown handshake_key: "
|
|
f"{session_hash}: {handshake_key}"
|
|
)
|
|
|
|
_require_string(
|
|
credential.get("password"),
|
|
f"credentials.json[{index}].password",
|
|
)
|
|
|
|
if not credential.get("password"):
|
|
raise WifitripError(
|
|
"Credential password must not be empty."
|
|
)
|
|
|
|
_require_string(
|
|
credential.get("source"),
|
|
f"credentials.json[{index}].source",
|
|
allow_none=True,
|
|
)
|
|
|
|
_require_string(
|
|
credential.get("created_at"),
|
|
f"credentials.json[{index}].created_at",
|
|
allow_none=True,
|
|
)
|
|
|
|
verified = credential.get(
|
|
"verified"
|
|
)
|
|
|
|
_require_integer(
|
|
verified,
|
|
f"credentials.json[{index}].verified",
|
|
allow_none=False,
|
|
)
|
|
|
|
if verified not in (0, 1):
|
|
raise WifitripError(
|
|
"Credential verified must be 0 or 1."
|
|
)
|
|
|
|
|
|
def _session_hash(conn, session_id):
|
|
session = conn.execute(
|
|
"""
|
|
SELECT
|
|
start_time,
|
|
end_time
|
|
FROM capture_sessions
|
|
WHERE id=?
|
|
""",
|
|
(session_id,),
|
|
).fetchone()
|
|
|
|
if session is None:
|
|
raise WifitripError(
|
|
f"Session {session_id} does not exist."
|
|
)
|
|
|
|
gps_rows = conn.execute(
|
|
"""
|
|
SELECT
|
|
timestamp,
|
|
latitude,
|
|
longitude
|
|
FROM gps_track
|
|
WHERE session_id=?
|
|
ORDER BY sequence_no
|
|
""",
|
|
(session_id,),
|
|
).fetchall()
|
|
|
|
handshake_rows = 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 handshake_rows
|
|
]
|
|
|
|
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 = 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,
|
|
]
|
|
|
|
return hashlib.sha256(
|
|
_json_bytes(canonical)
|
|
).hexdigest()
|
|
|
|
|
|
def _default_export_path():
|
|
timestamp = datetime.now(
|
|
timezone.utc
|
|
).strftime(
|
|
"%Y%m%d-%H%M%S"
|
|
)
|
|
|
|
return Path.cwd() / (
|
|
"wifi-gps-mapper-trips-"
|
|
f"{timestamp}.wifitrip"
|
|
)
|
|
|
|
|
|
def _zip_write_json(archive, filename, value):
|
|
archive.writestr(
|
|
filename,
|
|
_json_bytes(value),
|
|
)
|
|
|
|
|
|
def export_wifitrip(conn, output_path=None):
|
|
started = time.monotonic()
|
|
|
|
if output_path is None:
|
|
output_path = _default_export_path()
|
|
|
|
output_path = Path(output_path)
|
|
|
|
if output_path.suffix.lower() != ".wifitrip":
|
|
output_path = output_path.with_suffix(
|
|
".wifitrip"
|
|
)
|
|
|
|
output_path.parent.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
sessions = conn.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
start_time,
|
|
end_time,
|
|
session_hash,
|
|
session_hash_version
|
|
FROM capture_sessions
|
|
ORDER BY id
|
|
"""
|
|
).fetchall()
|
|
|
|
session_by_id = {}
|
|
exported_sessions = []
|
|
|
|
for row in sessions:
|
|
session_id = row["id"]
|
|
session_hash = row["session_hash"]
|
|
hash_version = row[
|
|
"session_hash_version"
|
|
]
|
|
|
|
if not session_hash or hash_version is None:
|
|
raise WifitripError(
|
|
"Session "
|
|
f"{session_id} has no session hash."
|
|
)
|
|
|
|
if hash_version != SESSION_HASH_VERSION:
|
|
raise WifitripError(
|
|
"Session "
|
|
f"{session_id} has unsupported session hash "
|
|
f"version {hash_version}."
|
|
)
|
|
|
|
_validate_hash(
|
|
session_hash,
|
|
f"session {session_id} hash",
|
|
)
|
|
|
|
calculated_hash = _session_hash(
|
|
conn,
|
|
session_id,
|
|
)
|
|
|
|
if calculated_hash != session_hash:
|
|
raise WifitripError(
|
|
"Session "
|
|
f"{session_id} hash verification failed: "
|
|
f"expected {session_hash}, "
|
|
f"calculated {calculated_hash}"
|
|
)
|
|
|
|
gps_count = conn.execute(
|
|
"""
|
|
SELECT COUNT(*)
|
|
FROM gps_track
|
|
WHERE session_id=?
|
|
""",
|
|
(session_id,),
|
|
).fetchone()[0]
|
|
|
|
session_type = (
|
|
"gps"
|
|
if gps_count
|
|
else "non_gps"
|
|
)
|
|
|
|
session_record = {
|
|
"session_hash": session_hash,
|
|
"session_hash_version": hash_version,
|
|
"start_time": row["start_time"],
|
|
"end_time": row["end_time"],
|
|
"session_type": session_type,
|
|
}
|
|
|
|
session_by_id[session_id] = session_record
|
|
exported_sessions.append(
|
|
session_record
|
|
)
|
|
|
|
session_ids = list(session_by_id)
|
|
|
|
if not session_ids:
|
|
raise WifitripError(
|
|
"No capture sessions available for export."
|
|
)
|
|
|
|
placeholders = ",".join(
|
|
"?" for _ in session_ids
|
|
)
|
|
|
|
gps_rows = conn.execute(
|
|
f"""
|
|
SELECT
|
|
gps_track.session_id,
|
|
gps_track.sequence_no,
|
|
gps_track.timestamp,
|
|
gps_track.latitude,
|
|
gps_track.longitude,
|
|
gps_track.speed
|
|
FROM gps_track
|
|
WHERE gps_track.session_id IN (
|
|
{placeholders}
|
|
)
|
|
ORDER BY
|
|
gps_track.session_id,
|
|
gps_track.sequence_no
|
|
""",
|
|
session_ids,
|
|
).fetchall()
|
|
|
|
gps_tracks = [
|
|
{
|
|
"session_hash": session_by_id[
|
|
row["session_id"]
|
|
]["session_hash"],
|
|
"sequence_no": row["sequence_no"],
|
|
"timestamp": row["timestamp"],
|
|
"latitude": row["latitude"],
|
|
"longitude": row["longitude"],
|
|
"speed": row["speed"],
|
|
}
|
|
for row in gps_rows
|
|
]
|
|
|
|
observation_rows = conn.execute(
|
|
f"""
|
|
SELECT
|
|
observations.session_id,
|
|
access_points.bssid,
|
|
observations.observed_at,
|
|
observations.latitude,
|
|
observations.longitude,
|
|
observations.speed,
|
|
observations.rssi,
|
|
observations.channel,
|
|
observations.frequency,
|
|
observations.essid,
|
|
observations.encryption,
|
|
observations.cipher,
|
|
observations.akm,
|
|
observations.country,
|
|
observations.id
|
|
FROM access_point_observations AS observations
|
|
JOIN access_points
|
|
ON access_points.id =
|
|
observations.access_point_id
|
|
WHERE observations.session_id IN (
|
|
{placeholders}
|
|
)
|
|
ORDER BY
|
|
observations.session_id,
|
|
observations.observed_at,
|
|
observations.id
|
|
""",
|
|
session_ids,
|
|
).fetchall()
|
|
|
|
observations = [
|
|
{
|
|
"session_hash": session_by_id[
|
|
row["session_id"]
|
|
]["session_hash"],
|
|
"bssid": row["bssid"],
|
|
"observed_at": row["observed_at"],
|
|
"latitude": row["latitude"],
|
|
"longitude": row["longitude"],
|
|
"speed": row["speed"],
|
|
"rssi": row["rssi"],
|
|
"channel": row["channel"],
|
|
"frequency": row["frequency"],
|
|
"essid": row["essid"],
|
|
"encryption": row["encryption"],
|
|
"cipher": row["cipher"],
|
|
"akm": row["akm"],
|
|
"country": row["country"],
|
|
}
|
|
for row in observation_rows
|
|
]
|
|
|
|
handshake_rows = conn.execute(
|
|
f"""
|
|
SELECT
|
|
handshakes.id,
|
|
handshakes.session_id,
|
|
access_points.bssid,
|
|
handshakes.type,
|
|
handshakes.hash22000,
|
|
handshakes.message_pair,
|
|
handshakes.captured_at,
|
|
handshakes.latitude,
|
|
handshakes.longitude,
|
|
handshakes.speed
|
|
FROM handshakes
|
|
JOIN access_points
|
|
ON access_points.id =
|
|
handshakes.access_point_id
|
|
WHERE handshakes.session_id IN (
|
|
{placeholders}
|
|
)
|
|
ORDER BY
|
|
handshakes.session_id,
|
|
handshakes.id
|
|
""",
|
|
session_ids,
|
|
).fetchall()
|
|
|
|
handshake_key_by_id = {}
|
|
handshake_records = {}
|
|
|
|
handshakes = []
|
|
|
|
for row in handshake_rows:
|
|
session_hash = session_by_id[
|
|
row["session_id"]
|
|
]["session_hash"]
|
|
|
|
records = handshake_records.setdefault(
|
|
session_hash,
|
|
[],
|
|
)
|
|
|
|
handshake_key = (
|
|
f"h{len(records) + 1:06d}"
|
|
)
|
|
|
|
handshake_key_by_id[
|
|
row["id"]
|
|
] = (
|
|
session_hash,
|
|
handshake_key,
|
|
)
|
|
|
|
records.append(
|
|
handshake_key
|
|
)
|
|
|
|
handshakes.append(
|
|
{
|
|
"session_hash": session_hash,
|
|
"handshake_key": handshake_key,
|
|
"bssid": row["bssid"],
|
|
"type": row["type"],
|
|
"hash22000": row["hash22000"],
|
|
"message_pair": row["message_pair"],
|
|
"captured_at": row["captured_at"],
|
|
"latitude": row["latitude"],
|
|
"longitude": row["longitude"],
|
|
"speed": row["speed"],
|
|
}
|
|
)
|
|
|
|
session_ap_ids = {}
|
|
|
|
for row in observation_rows:
|
|
session_ap_ids.setdefault(
|
|
row["session_id"],
|
|
set(),
|
|
).add(
|
|
row["bssid"]
|
|
)
|
|
|
|
for row in handshake_rows:
|
|
session_ap_ids.setdefault(
|
|
row["session_id"],
|
|
set(),
|
|
).add(
|
|
row["bssid"]
|
|
)
|
|
|
|
credentials_rows = conn.execute(
|
|
"""
|
|
SELECT
|
|
credentials.id,
|
|
credentials.access_point_id,
|
|
credentials.handshake_id,
|
|
access_points.bssid,
|
|
credentials.password,
|
|
credentials.source,
|
|
credentials.created_at,
|
|
credentials.verified,
|
|
handshakes.session_id AS handshake_session_id
|
|
FROM credentials
|
|
JOIN access_points
|
|
ON access_points.id =
|
|
credentials.access_point_id
|
|
LEFT JOIN handshakes
|
|
ON handshakes.id =
|
|
credentials.handshake_id
|
|
ORDER BY credentials.id
|
|
"""
|
|
).fetchall()
|
|
|
|
credentials = []
|
|
|
|
for row in credentials_rows:
|
|
if row["handshake_session_id"] is not None:
|
|
session_id = row[
|
|
"handshake_session_id"
|
|
]
|
|
|
|
if session_id not in session_by_id:
|
|
continue
|
|
|
|
session_hash = session_by_id[
|
|
session_id
|
|
]["session_hash"]
|
|
|
|
handshake_ref = (
|
|
handshake_key_by_id.get(
|
|
row["handshake_id"]
|
|
)
|
|
)
|
|
|
|
handshake_key = (
|
|
handshake_ref[1]
|
|
if handshake_ref
|
|
else None
|
|
)
|
|
|
|
credentials.append(
|
|
{
|
|
"session_hash": session_hash,
|
|
"bssid": row["bssid"],
|
|
"handshake_key": handshake_key,
|
|
"password": row["password"],
|
|
"source": row["source"],
|
|
"created_at": row["created_at"],
|
|
"verified": row["verified"],
|
|
}
|
|
)
|
|
|
|
continue
|
|
|
|
for session_id, bssids in (
|
|
session_ap_ids.items()
|
|
):
|
|
if row["bssid"] not in bssids:
|
|
continue
|
|
|
|
if session_id not in session_by_id:
|
|
continue
|
|
|
|
credentials.append(
|
|
{
|
|
"session_hash": session_by_id[
|
|
session_id
|
|
]["session_hash"],
|
|
"bssid": row["bssid"],
|
|
"handshake_key": None,
|
|
"password": row["password"],
|
|
"source": row["source"],
|
|
"created_at": row["created_at"],
|
|
"verified": row["verified"],
|
|
}
|
|
)
|
|
|
|
bssids = set()
|
|
|
|
for observation in observations:
|
|
bssids.add(
|
|
observation["bssid"]
|
|
)
|
|
|
|
for handshake in handshakes:
|
|
bssids.add(
|
|
handshake["bssid"]
|
|
)
|
|
|
|
for credential in credentials:
|
|
bssids.add(
|
|
credential["bssid"]
|
|
)
|
|
|
|
access_points = [
|
|
{
|
|
"bssid": bssid
|
|
}
|
|
for bssid in sorted(bssids)
|
|
]
|
|
|
|
manifest = {
|
|
"format": WIFITRIP_FORMAT,
|
|
"format_version": WIFITRIP_VERSION,
|
|
"created_at": time.strftime(
|
|
"%Y-%m-%dT%H:%M:%SZ",
|
|
time.gmtime(),
|
|
),
|
|
"sessions": len(
|
|
exported_sessions
|
|
),
|
|
"session_hash_versions": [
|
|
SESSION_HASH_VERSION
|
|
],
|
|
}
|
|
|
|
logger.info(
|
|
"Starting .wifitrip export: sessions=%d file=%s",
|
|
len(exported_sessions),
|
|
output_path,
|
|
)
|
|
|
|
fd, temporary_name = tempfile.mkstemp(
|
|
prefix=".wifitrip-",
|
|
suffix=".tmp",
|
|
dir=str(output_path.parent),
|
|
)
|
|
|
|
os.close(fd)
|
|
|
|
temporary_path = Path(
|
|
temporary_name
|
|
)
|
|
|
|
try:
|
|
with zipfile.ZipFile(
|
|
temporary_path,
|
|
"w",
|
|
compression=zipfile.ZIP_DEFLATED,
|
|
compresslevel=6,
|
|
) as archive:
|
|
_zip_write_json(
|
|
archive,
|
|
"manifest.json",
|
|
manifest,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"sessions.json",
|
|
exported_sessions,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"gps_tracks.json",
|
|
gps_tracks,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"access_points.json",
|
|
access_points,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"observations.json",
|
|
observations,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"handshakes.json",
|
|
handshakes,
|
|
)
|
|
|
|
_zip_write_json(
|
|
archive,
|
|
"credentials.json",
|
|
credentials,
|
|
)
|
|
|
|
os.replace(
|
|
temporary_path,
|
|
output_path,
|
|
)
|
|
|
|
except Exception:
|
|
try:
|
|
temporary_path.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
raise
|
|
|
|
size = output_path.stat().st_size
|
|
duration = (
|
|
time.monotonic() - started
|
|
)
|
|
|
|
result = {
|
|
"sessions": len(
|
|
exported_sessions
|
|
),
|
|
"access_points": len(
|
|
access_points
|
|
),
|
|
"gps_points": len(
|
|
gps_tracks
|
|
),
|
|
"observations": len(
|
|
observations
|
|
),
|
|
"handshakes": len(
|
|
handshakes
|
|
),
|
|
"credentials": len(
|
|
credentials
|
|
),
|
|
"file": str(output_path),
|
|
"size": size,
|
|
"duration": duration,
|
|
}
|
|
|
|
logger.info(
|
|
"Completed .wifitrip export: "
|
|
"sessions=%d observations=%d handshakes=%d "
|
|
"credentials=%d file=%s size=%d duration=%.3fs",
|
|
result["sessions"],
|
|
result["observations"],
|
|
result["handshakes"],
|
|
result["credentials"],
|
|
output_path,
|
|
size,
|
|
duration,
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def _get_or_create_import_ap(
|
|
conn,
|
|
bssid,
|
|
created_access_points,
|
|
):
|
|
row = conn.execute(
|
|
"""
|
|
SELECT id
|
|
FROM access_points
|
|
WHERE bssid=?
|
|
""",
|
|
(bssid,),
|
|
).fetchone()
|
|
|
|
if row is not None:
|
|
return row["id"]
|
|
|
|
cursor = conn.execute(
|
|
"""
|
|
INSERT INTO access_points (
|
|
bssid,
|
|
times_seen
|
|
)
|
|
VALUES (?, 0)
|
|
""",
|
|
(bssid,),
|
|
)
|
|
|
|
access_point_id = cursor.lastrowid
|
|
|
|
created_access_points.add(
|
|
access_point_id
|
|
)
|
|
|
|
return access_point_id
|
|
|
|
|
|
def _update_imported_ap_aggregates(
|
|
conn,
|
|
access_point_id,
|
|
imported_observation_count,
|
|
imported_first_seen,
|
|
imported_last_seen,
|
|
latest_observation,
|
|
):
|
|
if imported_observation_count == 0:
|
|
return
|
|
|
|
existing = conn.execute(
|
|
"""
|
|
SELECT
|
|
first_seen,
|
|
last_seen,
|
|
times_seen
|
|
FROM access_points
|
|
WHERE id=?
|
|
""",
|
|
(access_point_id,),
|
|
).fetchone()
|
|
|
|
if existing is None:
|
|
raise WifitripError(
|
|
f"Access point {access_point_id} does not exist."
|
|
)
|
|
|
|
first_seen = existing["first_seen"]
|
|
|
|
if (
|
|
first_seen is None
|
|
or imported_first_seen < first_seen
|
|
):
|
|
first_seen = imported_first_seen
|
|
|
|
last_seen = existing["last_seen"]
|
|
|
|
latest_is_global_latest = (
|
|
last_seen is None
|
|
or imported_last_seen >= last_seen
|
|
)
|
|
|
|
if (
|
|
last_seen is None
|
|
or imported_last_seen > last_seen
|
|
):
|
|
last_seen = imported_last_seen
|
|
|
|
if latest_is_global_latest:
|
|
conn.execute(
|
|
"""
|
|
UPDATE access_points
|
|
SET
|
|
first_seen=?,
|
|
last_seen=?,
|
|
times_seen=COALESCE(times_seen, 0)
|
|
+ ?,
|
|
last_rssi=?,
|
|
last_latitude=?,
|
|
last_longitude=?,
|
|
last_speed=?,
|
|
channel=COALESCE(?, channel),
|
|
frequency=COALESCE(?, frequency),
|
|
essid=COALESCE(?, essid),
|
|
encryption=COALESCE(?, encryption),
|
|
cipher=COALESCE(?, cipher),
|
|
akm=COALESCE(?, akm),
|
|
country=COALESCE(?, country)
|
|
WHERE id=?
|
|
""",
|
|
(
|
|
first_seen,
|
|
last_seen,
|
|
imported_observation_count,
|
|
latest_observation["rssi"],
|
|
latest_observation["latitude"],
|
|
latest_observation["longitude"],
|
|
latest_observation["speed"],
|
|
latest_observation["channel"],
|
|
latest_observation["frequency"],
|
|
latest_observation["essid"],
|
|
latest_observation["encryption"],
|
|
latest_observation["cipher"],
|
|
latest_observation["akm"],
|
|
latest_observation["country"],
|
|
access_point_id,
|
|
),
|
|
)
|
|
|
|
else:
|
|
conn.execute(
|
|
"""
|
|
UPDATE access_points
|
|
SET
|
|
first_seen=?,
|
|
last_seen=?,
|
|
times_seen=COALESCE(times_seen, 0)
|
|
+ ?
|
|
WHERE id=?
|
|
""",
|
|
(
|
|
first_seen,
|
|
last_seen,
|
|
imported_observation_count,
|
|
access_point_id,
|
|
),
|
|
)
|
|
|
|
|
|
def _session_id_by_hash(
|
|
conn,
|
|
session_hash,
|
|
session_hash_version,
|
|
):
|
|
row = conn.execute(
|
|
"""
|
|
SELECT id
|
|
FROM capture_sessions
|
|
WHERE session_hash=?
|
|
AND session_hash_version=?
|
|
LIMIT 1
|
|
""",
|
|
(
|
|
session_hash,
|
|
session_hash_version,
|
|
),
|
|
).fetchone()
|
|
|
|
if row is None:
|
|
return None
|
|
|
|
return row["id"]
|
|
|
|
|
|
def import_wifitrip(conn, package_path):
|
|
started = time.monotonic()
|
|
|
|
logger.info(
|
|
"Starting .wifitrip import: file=%s",
|
|
package_path,
|
|
)
|
|
|
|
package = _read_package(
|
|
package_path
|
|
)
|
|
|
|
manifest = package["manifest.json"]
|
|
sessions = package["sessions.json"]
|
|
gps_tracks = package["gps_tracks.json"]
|
|
observations = package["observations.json"]
|
|
handshakes = package["handshakes.json"]
|
|
credentials = package["credentials.json"]
|
|
|
|
gps_by_session = {}
|
|
observations_by_session = {}
|
|
handshakes_by_session = {}
|
|
credentials_by_session = {}
|
|
|
|
for point in gps_tracks:
|
|
gps_by_session.setdefault(
|
|
point["session_hash"],
|
|
[],
|
|
).append(point)
|
|
|
|
for observation in observations:
|
|
observations_by_session.setdefault(
|
|
observation["session_hash"],
|
|
[],
|
|
).append(observation)
|
|
|
|
for handshake in handshakes:
|
|
handshakes_by_session.setdefault(
|
|
handshake["session_hash"],
|
|
[],
|
|
).append(handshake)
|
|
|
|
for credential in credentials:
|
|
credentials_by_session.setdefault(
|
|
credential["session_hash"],
|
|
[],
|
|
).append(credential)
|
|
|
|
result = {
|
|
"file": str(package_path),
|
|
"sessions": len(sessions),
|
|
"imported": 0,
|
|
"duplicates": 0,
|
|
"failed": 0,
|
|
"failed_sessions": [],
|
|
}
|
|
|
|
conn.execute(
|
|
"PRAGMA foreign_keys=ON"
|
|
)
|
|
|
|
if conn.in_transaction:
|
|
raise WifitripError(
|
|
"Database connection already has an active transaction."
|
|
)
|
|
|
|
for session in sessions:
|
|
session_hash = session[
|
|
"session_hash"
|
|
]
|
|
hash_version = session[
|
|
"session_hash_version"
|
|
]
|
|
|
|
existing_id = _session_id_by_hash(
|
|
conn,
|
|
session_hash,
|
|
hash_version,
|
|
)
|
|
|
|
if existing_id is not None:
|
|
result["duplicates"] += 1
|
|
|
|
logger.info(
|
|
"Skipping duplicate .wifitrip session: "
|
|
"hash=%s local_id=%s",
|
|
session_hash,
|
|
existing_id,
|
|
)
|
|
|
|
continue
|
|
|
|
try:
|
|
conn.execute(
|
|
"BEGIN"
|
|
)
|
|
|
|
cursor = conn.execute(
|
|
"""
|
|
INSERT INTO capture_sessions (
|
|
start_time,
|
|
end_time,
|
|
capture_file,
|
|
session_hash,
|
|
session_hash_version
|
|
)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
session["start_time"],
|
|
session["end_time"],
|
|
f"wifitrip:{session_hash}",
|
|
session_hash,
|
|
hash_version,
|
|
),
|
|
)
|
|
|
|
local_session_id = cursor.lastrowid
|
|
|
|
created_access_points = set()
|
|
ap_ids = {}
|
|
|
|
session_observations = (
|
|
observations_by_session.get(
|
|
session_hash,
|
|
[],
|
|
)
|
|
)
|
|
|
|
session_handshakes = (
|
|
handshakes_by_session.get(
|
|
session_hash,
|
|
[],
|
|
)
|
|
)
|
|
|
|
session_credentials = (
|
|
credentials_by_session.get(
|
|
session_hash,
|
|
[],
|
|
)
|
|
)
|
|
|
|
bssids = set()
|
|
|
|
for observation in session_observations:
|
|
bssids.add(
|
|
observation["bssid"]
|
|
)
|
|
|
|
for handshake in session_handshakes:
|
|
bssids.add(
|
|
handshake["bssid"]
|
|
)
|
|
|
|
for credential in session_credentials:
|
|
bssids.add(
|
|
credential["bssid"]
|
|
)
|
|
|
|
for bssid in sorted(bssids):
|
|
ap_ids[bssid] = (
|
|
_get_or_create_import_ap(
|
|
conn,
|
|
bssid,
|
|
created_access_points,
|
|
)
|
|
)
|
|
|
|
for point in gps_by_session.get(
|
|
session_hash,
|
|
[],
|
|
):
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO gps_track (
|
|
session_id,
|
|
sequence_no,
|
|
latitude,
|
|
longitude,
|
|
speed,
|
|
timestamp
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
local_session_id,
|
|
point["sequence_no"],
|
|
point["latitude"],
|
|
point["longitude"],
|
|
point["speed"],
|
|
point["timestamp"],
|
|
),
|
|
)
|
|
|
|
imported_observations_by_ap = {}
|
|
|
|
for observation in session_observations:
|
|
access_point_id = ap_ids[
|
|
observation["bssid"]
|
|
]
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO access_point_observations (
|
|
access_point_id,
|
|
session_id,
|
|
observed_at,
|
|
latitude,
|
|
longitude,
|
|
speed,
|
|
rssi,
|
|
channel,
|
|
frequency,
|
|
essid,
|
|
encryption,
|
|
cipher,
|
|
akm,
|
|
country
|
|
)
|
|
VALUES (
|
|
?, ?, ?, ?, ?, ?, ?, ?,
|
|
?, ?, ?, ?, ?, ?
|
|
)
|
|
""",
|
|
(
|
|
access_point_id,
|
|
local_session_id,
|
|
observation["observed_at"],
|
|
observation["latitude"],
|
|
observation["longitude"],
|
|
observation["speed"],
|
|
observation["rssi"],
|
|
observation["channel"],
|
|
observation["frequency"],
|
|
observation["essid"],
|
|
observation["encryption"],
|
|
observation["cipher"],
|
|
observation["akm"],
|
|
observation["country"],
|
|
),
|
|
)
|
|
|
|
imported_observations_by_ap.setdefault(
|
|
access_point_id,
|
|
[],
|
|
).append(observation)
|
|
|
|
for access_point_id, imported_observations in (
|
|
imported_observations_by_ap.items()
|
|
):
|
|
imported_first_seen = min(
|
|
observation["observed_at"]
|
|
for observation in imported_observations
|
|
)
|
|
|
|
imported_last_seen = max(
|
|
observation["observed_at"]
|
|
for observation in imported_observations
|
|
)
|
|
|
|
latest_observation = max(
|
|
imported_observations,
|
|
key=lambda observation: (
|
|
observation["observed_at"],
|
|
),
|
|
)
|
|
|
|
_update_imported_ap_aggregates(
|
|
conn,
|
|
access_point_id,
|
|
len(imported_observations),
|
|
imported_first_seen,
|
|
imported_last_seen,
|
|
latest_observation,
|
|
)
|
|
|
|
handshake_ids = {}
|
|
|
|
for handshake in session_handshakes:
|
|
access_point_id = ap_ids[
|
|
handshake["bssid"]
|
|
]
|
|
|
|
cursor = conn.execute(
|
|
"""
|
|
INSERT INTO handshakes (
|
|
access_point_id,
|
|
session_id,
|
|
type,
|
|
hash22000,
|
|
message_pair,
|
|
captured_at,
|
|
latitude,
|
|
longitude,
|
|
speed
|
|
)
|
|
VALUES (
|
|
?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
)
|
|
""",
|
|
(
|
|
access_point_id,
|
|
local_session_id,
|
|
handshake["type"],
|
|
handshake["hash22000"],
|
|
handshake["message_pair"],
|
|
handshake["captured_at"],
|
|
handshake["latitude"],
|
|
handshake["longitude"],
|
|
handshake["speed"],
|
|
),
|
|
)
|
|
|
|
handshake_ids[
|
|
handshake["handshake_key"]
|
|
] = cursor.lastrowid
|
|
|
|
if handshake["type"] == "PMKID":
|
|
conn.execute(
|
|
"""
|
|
UPDATE access_points
|
|
SET
|
|
has_pmkid=1
|
|
WHERE id=?
|
|
""",
|
|
(access_point_id,),
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"""
|
|
UPDATE access_points
|
|
SET
|
|
has_handshake=1
|
|
WHERE id=?
|
|
""",
|
|
(access_point_id,),
|
|
)
|
|
|
|
calculated_hash = _session_hash(
|
|
conn,
|
|
local_session_id,
|
|
)
|
|
|
|
if calculated_hash != session_hash:
|
|
raise WifitripError(
|
|
"Session hash verification failed: "
|
|
f"expected {session_hash}, "
|
|
f"calculated {calculated_hash}"
|
|
)
|
|
|
|
for credential in session_credentials:
|
|
access_point_id = ap_ids[
|
|
credential["bssid"]
|
|
]
|
|
|
|
handshake_id = None
|
|
|
|
if credential[
|
|
"handshake_key"
|
|
] is not None:
|
|
handshake_id = handshake_ids[
|
|
credential[
|
|
"handshake_key"
|
|
]
|
|
]
|
|
|
|
existing_credential = conn.execute(
|
|
"""
|
|
SELECT id
|
|
FROM credentials
|
|
WHERE access_point_id=?
|
|
AND handshake_id IS ?
|
|
AND password=?
|
|
LIMIT 1
|
|
""",
|
|
(
|
|
access_point_id,
|
|
handshake_id,
|
|
credential["password"],
|
|
),
|
|
).fetchone()
|
|
|
|
if existing_credential is not None:
|
|
continue
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO credentials (
|
|
access_point_id,
|
|
handshake_id,
|
|
password,
|
|
source,
|
|
created_at,
|
|
verified
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
access_point_id,
|
|
handshake_id,
|
|
credential["password"],
|
|
credential["source"],
|
|
credential["created_at"],
|
|
credential["verified"],
|
|
),
|
|
)
|
|
|
|
conn.execute(
|
|
"""
|
|
UPDATE access_points
|
|
SET
|
|
is_cracked=1
|
|
WHERE id=?
|
|
""",
|
|
(access_point_id,),
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
result["imported"] += 1
|
|
|
|
logger.info(
|
|
"Imported .wifitrip session: "
|
|
"hash=%s local_id=%s",
|
|
session_hash,
|
|
local_session_id,
|
|
)
|
|
|
|
except Exception as exc:
|
|
conn.rollback()
|
|
|
|
result["failed"] += 1
|
|
result["failed_sessions"].append(
|
|
{
|
|
"session_hash": session_hash,
|
|
"error": str(exc),
|
|
}
|
|
)
|
|
|
|
logger.exception(
|
|
"Failed to import .wifitrip session: "
|
|
"hash=%s",
|
|
session_hash,
|
|
)
|
|
|
|
result["duration"] = (
|
|
time.monotonic() - started
|
|
)
|
|
|
|
logger.info(
|
|
"Completed .wifitrip import: "
|
|
"sessions=%d imported=%d duplicates=%d "
|
|
"failed=%d file=%s duration=%.3fs",
|
|
result["sessions"],
|
|
result["imported"],
|
|
result["duplicates"],
|
|
result["failed"],
|
|
package_path,
|
|
result["duration"],
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def validate_wifitrip(package_path):
|
|
"""
|
|
Полная проверка .wifitrip без изменения БД.
|
|
|
|
Возвращает краткую информацию о пакете.
|
|
"""
|
|
package = _read_package(
|
|
package_path
|
|
)
|
|
|
|
manifest = package["manifest.json"]
|
|
|
|
return {
|
|
"valid": True,
|
|
"format": manifest["format"],
|
|
"format_version": manifest[
|
|
"format_version"
|
|
],
|
|
"sessions": len(
|
|
package["sessions.json"]
|
|
),
|
|
"access_points": len(
|
|
package["access_points.json"]
|
|
),
|
|
"gps_points": len(
|
|
package["gps_tracks.json"]
|
|
),
|
|
"observations": len(
|
|
package["observations.json"]
|
|
),
|
|
"handshakes": len(
|
|
package["handshakes.json"]
|
|
),
|
|
"credentials": len(
|
|
package["credentials.json"]
|
|
),
|
|
"session_hash_versions": manifest[
|
|
"session_hash_versions"
|
|
],
|
|
}
|