Update public source

This commit is contained in:
2026-10-06 21:08:12 +03:00
commit f231cc5b1f
102 changed files with 48249 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
from .job_issuance import JobIssuanceService, IssuedJob
from .job_result import JobResultError, JobResultService
from .methods import MethodService, MethodServiceError
__all__ = [
"IssuedJob",
"JobIssuanceService",
"JobResultError",
"JobResultService",
"MethodService",
"MethodServiceError",
]
+348
View File
@@ -0,0 +1,348 @@
from __future__ import annotations
import hashlib
import json
import tempfile
import zipfile
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
import yaml
from cracklab.server.repositories.jobs import JobsRepository
from cracklab.server.repositories.methods import MethodsRepository
from cracklab.server.services.method_definition import (
validate_method_definition,
)
PROJECT_ROOT = Path(__file__).resolve().parents[3]
CONFIG_FILE = PROJECT_ROOT / "config" / "config.yaml"
class JobExportError(RuntimeError):
pass
class JobExportService:
def __init__(
self,
conn,
timezone_name: str | None = None,
) -> None:
self._conn = conn
self._jobs = JobsRepository(conn)
self._methods = MethodsRepository(conn)
self._timezone_name = (
timezone_name
if timezone_name is not None
else self._load_timezone()
)
try:
self._timezone = ZoneInfo(self._timezone_name)
except Exception as exc:
raise JobExportError(
f"Invalid timezone: {self._timezone_name}"
) from exc
@staticmethod
def _load_timezone() -> str:
if not CONFIG_FILE.is_file():
raise JobExportError(
f"Configuration file not found: {CONFIG_FILE}"
)
try:
with CONFIG_FILE.open(
"r",
encoding="utf-8",
) as fp:
config = yaml.safe_load(fp) or {}
except Exception as exc:
raise JobExportError(
f"Failed to load configuration: {CONFIG_FILE}"
) from exc
timezone_name = config.get(
"timezone",
"Europe/Moscow",
)
if not isinstance(timezone_name, str) or not timezone_name:
raise JobExportError(
"Configuration timezone must be a non-empty string"
)
return timezone_name
@staticmethod
def _parse_utc(text: str) -> datetime:
try:
value = datetime.fromisoformat(text)
except ValueError as exc:
raise JobExportError(
f"Invalid job created_at: {text}"
) from exc
if value.tzinfo is None:
raise JobExportError(
f"Job timestamp is not timezone-aware: {text}"
)
value = value.astimezone(timezone.utc)
return value
@staticmethod
def _unique_hash_content(rows) -> tuple[str, int]:
unique_hashes: list[str] = []
seen: set[str] = set()
for row in rows:
hash22000 = str(row["hash22000"]).strip()
if not hash22000:
raise JobExportError(
"Job contains an empty hash22000 value"
)
if hash22000 in seen:
continue
seen.add(hash22000)
unique_hashes.append(hash22000)
content = "\n".join(unique_hashes) + "\n"
return content, len(unique_hashes)
@staticmethod
def _sha256_text(content: str) -> str:
return hashlib.sha256(
content.encode("utf-8")
).hexdigest()
@staticmethod
def _json_bytes(data: dict) -> bytes:
return (
json.dumps(
data,
ensure_ascii=False,
indent=2,
)
+ "\n"
).encode("utf-8")
def export_job(
self,
job_id: str,
output_dir: str | Path,
) -> Path:
job = self._jobs.get_job(job_id)
if job is None:
raise JobExportError(
f"Job not found: {job_id}"
)
method_version = self._methods.get_method_version(
str(job["method_id"]),
int(job["method_version"]),
)
if method_version is None:
raise JobExportError(
"Method version not found: "
f"{job['method_id']} v{job['method_version']}"
)
try:
method_definition = json.loads(
method_version["definition_json"]
)
validate_method_definition(
method_definition
)
except (json.JSONDecodeError, TypeError, ValueError) as exc:
raise JobExportError(
"Invalid method version definition: "
f"{job['method_id']} v{job['method_version']}"
) from exc
hash_mode = int(
method_definition["hash_mode"]
)
handshake_rows = self._jobs.list_job_handshakes(
job_id
)
step_rows = self._jobs.list_job_steps(
job_id
)
hash_content, hash_count = self._unique_hash_content(
handshake_rows
)
hash_sha256 = self._sha256_text(
hash_content
)
if hash_sha256 != job["hash_file_sha256"]:
raise JobExportError(
"Job hash snapshot SHA-256 mismatch: "
f"expected {job['hash_file_sha256']}, "
f"calculated {hash_sha256}"
)
if hash_count != int(job["hash_count"]):
raise JobExportError(
"Job hash count mismatch: "
f"expected {job['hash_count']}, "
f"calculated {hash_count}"
)
if len(handshake_rows) == 0:
raise JobExportError(
f"Job contains no handshake snapshot: {job_id}"
)
if len(step_rows) != int(job["step_count"]):
raise JobExportError(
"Job step count mismatch: "
f"expected {job['step_count']}, "
f"calculated {len(step_rows)}"
)
created_at = self._parse_utc(
str(job["created_at"])
)
local_created_at = created_at.astimezone(
self._timezone
)
zip_filename = (
"JOB-"
+ local_created_at.strftime(
"%d-%m-%Y_%H%M%S"
)
+ "-"
+ str(job["id"]).removeprefix("JOB-")
+ ".zip"
)
job_json = {
"job_id": str(job["id"]),
"created_at": str(job["created_at"]),
"method_id": str(job["method_id"]),
"method_version": int(job["method_version"]),
"hash_mode": hash_mode,
"hash_file_name": str(job["hash_file_name"]),
"hash_file_sha256": str(job["hash_file_sha256"]),
"hash_count": int(job["hash_count"]),
"handshake_count": len(handshake_rows),
"step_count": int(job["step_count"]),
"handshakes": [
{
"handshake_id": int(row["handshake_id"]),
"hash22000": str(row["hash22000"]),
"access_point_id": (
int(row["access_point_id"])
if row["access_point_id"] is not None
else None
),
}
for row in handshake_rows
],
"steps": [
{
"step_no": int(row["step_no"]),
"step_id": str(row["step_id"]),
"session_name": str(row["session_name"]),
"definition": json.loads(
row["definition_json"]
),
}
for row in step_rows
],
}
job_bytes = self._json_bytes(job_json)
hash_bytes = hash_content.encode("utf-8")
manifest_files = [
{
"path": "job.json",
"size": len(job_bytes),
"sha256": hashlib.sha256(
job_bytes
).hexdigest(),
},
{
"path": f"hashes/{job['hash_file_name']}",
"size": len(hash_bytes),
"sha256": hashlib.sha256(
hash_bytes
).hexdigest(),
},
]
manifest = {
"format_version": 1,
"job_id": str(job["id"]),
"created_at": str(job["created_at"]),
"zip_filename": zip_filename,
"files": manifest_files,
}
manifest_bytes = self._json_bytes(
manifest
)
output_path = Path(output_dir)
output_path.mkdir(
parents=True,
exist_ok=True,
)
final_path = output_path / zip_filename
with tempfile.NamedTemporaryFile(
mode="wb",
suffix=".zip",
dir=output_path,
delete=False,
) as tmp:
temporary_path = Path(tmp.name)
try:
with zipfile.ZipFile(
temporary_path,
mode="w",
compression=zipfile.ZIP_DEFLATED,
) as archive:
archive.writestr(
"job.json",
job_bytes,
)
archive.writestr(
f"hashes/{job['hash_file_name']}",
hash_bytes,
)
archive.writestr(
"manifest.json",
manifest_bytes,
)
temporary_path.replace(final_path)
except Exception:
temporary_path.unlink(
missing_ok=True
)
raise
return final_path
+262
View File
@@ -0,0 +1,262 @@
from __future__ import annotations
import hashlib
import json
import sqlite3
import uuid
from dataclasses import dataclass
from typing import Any
from cracklab.server.repositories.audit import AuditRepository
from cracklab.server.repositories.handshakes import HandshakesRepository
from cracklab.server.repositories.jobs import JobsRepository
from cracklab.server.repositories.methods import MethodsRepository
from cracklab.server.repositories.reservations import ReservationsRepository
from cracklab.server.services.method_definition import (
MethodDefinitionError,
validate_method_definition,
)
class JobIssuanceError(ValueError):
pass
@dataclass(frozen=True)
class IssuedJob:
job_id: str
method_id: str
method_version: int
status: str
created_at: str
hash_file_name: str
hash_file_sha256: str
hash_count: int
handshake_count: int
step_count: int
hash_content: str
handshake_ids: tuple[int, ...]
steps: tuple[dict[str, Any], ...]
class JobIssuanceService:
def __init__(self, conn: sqlite3.Connection) -> None:
self._conn = conn
self._methods = MethodsRepository(conn)
self._handshakes = HandshakesRepository(conn)
self._jobs = JobsRepository(conn)
self._reservations = ReservationsRepository(conn)
self._audit = AuditRepository(conn)
def issue_job(
self,
method_id: str,
method_version: int,
limit: int,
client_id: str | None = None,
) -> IssuedJob | None:
if limit <= 0:
raise ValueError("limit must be greater than zero")
method_version_row = self._methods.get_method_version(
method_id,
method_version,
)
if method_version_row is None:
raise JobIssuanceError(
f"Method version not found: {method_id} v{method_version}"
)
definition = self._decode_method_definition(
method_version_row["definition_json"],
)
steps = self._build_steps(
method_id,
method_version,
definition,
)
self._conn.execute("BEGIN IMMEDIATE")
try:
handshake_rows = self._handshakes.list_eligible(
method_id,
method_version,
limit,
)
if not handshake_rows:
self._conn.rollback()
return None
job_id = f"JOB-{uuid.uuid4().hex[:12].upper()}"
hash_file_name = f"{job_id}.22000"
handshake_ids = tuple(
int(row["handshake_id"])
for row in handshake_rows
)
unique_hashes: list[str] = []
seen_hashes: set[str] = set()
for row in handshake_rows:
hash22000 = str(row["hash22000"]).strip()
if hash22000 not in seen_hashes:
seen_hashes.add(hash22000)
unique_hashes.append(hash22000)
hash_content = "\n".join(unique_hashes) + "\n"
hash_file_sha256 = hashlib.sha256(
hash_content.encode("utf-8"),
).hexdigest()
created_at = self._jobs.create_job(
job_id=job_id,
method_id=method_id,
method_version=method_version,
status="QUEUED",
hash_file_name=hash_file_name,
hash_file_sha256=hash_file_sha256,
hash_count=len(unique_hashes),
step_count=len(steps),
client_id=client_id,
)
for row in handshake_rows:
self._jobs.add_job_handshake(
job_id=job_id,
handshake_id=int(row["handshake_id"]),
hash22000=str(row["hash22000"]).strip(),
access_point_id=(
int(row["access_point_id"])
if row["access_point_id"] is not None
else None
),
)
self._reservations.create_reservation(
reservation_id=uuid.uuid4().hex,
handshake_id=int(row["handshake_id"]),
method_id=method_id,
method_version=method_version,
job_id=job_id,
client_id=client_id,
)
for step_no, step in enumerate(steps, start=1):
self._jobs.create_job_step(
job_id=job_id,
step_no=step_no,
step_id=step["step_id"],
status="PENDING",
session_name=self._session_name(
job_id,
step["step_id"],
),
definition=step,
)
self._audit.create_event(
event_type="JOB_ISSUED",
job_id=job_id,
client_id=client_id,
method_id=method_id,
method_version=method_version,
details={
"handshake_count": len(handshake_rows),
"hash_count": len(unique_hashes),
"step_count": len(steps),
"hash_file_name": hash_file_name,
},
)
self._conn.commit()
except Exception:
self._conn.rollback()
raise
return IssuedJob(
job_id=job_id,
method_id=method_id,
method_version=method_version,
status="QUEUED",
created_at=created_at,
hash_file_name=hash_file_name,
hash_file_sha256=hash_file_sha256,
hash_count=len(unique_hashes),
handshake_count=len(handshake_rows),
step_count=len(steps),
hash_content=hash_content,
handshake_ids=handshake_ids,
steps=tuple(steps),
)
@staticmethod
def _decode_method_definition(
definition_json: str,
) -> dict[str, Any]:
try:
definition = json.loads(definition_json)
except json.JSONDecodeError as exc:
raise JobIssuanceError(
"Method version contains invalid definition JSON"
) from exc
try:
return validate_method_definition(definition)
except MethodDefinitionError as exc:
raise JobIssuanceError(str(exc)) from exc
@staticmethod
def _build_steps(
method_id: str,
method_version: int,
definition: dict[str, Any],
) -> list[dict[str, Any]]:
del method_id
del method_version
source_steps = definition["steps"]
result: list[dict[str, Any]] = []
seen_step_ids: set[str] = set()
for step in source_steps:
if not isinstance(step, dict):
raise JobIssuanceError(
"Each method step must be a JSON object"
)
step_id = step.get("step_id")
if not isinstance(step_id, str) or not step_id:
raise JobIssuanceError(
"Each method step must have a non-empty step_id"
)
if step_id in seen_step_ids:
raise JobIssuanceError(
f"Duplicate step_id: {step_id}"
)
seen_step_ids.add(step_id)
attack_mode = step.get("attack_mode")
if not isinstance(attack_mode, int) or attack_mode < 0:
raise JobIssuanceError(
f"Invalid attack_mode for step: {step_id}"
)
result.append(dict(step))
return result
@staticmethod
def _session_name(
job_id: str,
step_id: str,
) -> str:
return f"cl_{job_id[:12]}_{step_id}"
+740
View File
@@ -0,0 +1,740 @@
from __future__ import annotations
import hashlib
import json
import tempfile
import zipfile
from collections import defaultdict
from pathlib import Path
from typing import Any
from cracklab.server.repositories.attempts import AttemptsRepository
from cracklab.server.repositories.audit import AuditRepository
from cracklab.server.repositories.credentials import CredentialsRepository
from cracklab.server.repositories.jobs import JobsRepository
from cracklab.server.repositories.reservations import ReservationsRepository
from cracklab.server.repositories.results import ResultsRepository
class JobResultError(ValueError):
pass
class JobResultService:
def __init__(self, conn) -> None:
self._conn = conn
self._attempts = AttemptsRepository(conn)
self._audit = AuditRepository(conn)
self._credentials = CredentialsRepository(conn)
self._jobs = JobsRepository(conn)
self._reservations = ReservationsRepository(conn)
self._results = ResultsRepository(conn)
def import_result(self, package_path: Path | str) -> dict[str, Any]:
package = Path(package_path)
if not package.is_file():
raise JobResultError(f"Result package not found: {package}")
with tempfile.TemporaryDirectory(prefix="cracklab-result-") as tmp:
root = Path(tmp)
try:
with zipfile.ZipFile(package, "r") as zf:
self._safe_extract(zf, root)
except zipfile.BadZipFile as exc:
raise JobResultError(
f"Invalid result package: {package}"
) from exc
result_path, show_path = self._find_result_files(root)
result = self._read_json(result_path)
self._validate_result_sha256(result)
job_id = result.get("job_id")
if not isinstance(job_id, str) or not job_id:
raise JobResultError("Result is missing job_id")
self._conn.execute("BEGIN IMMEDIATE")
try:
job = self._jobs.get_job(job_id)
if job is None:
raise JobResultError(f"Unknown Job: {job_id}")
idempotent = self._check_idempotency(job, result)
if idempotent:
self._conn.rollback()
return {
"job_id": job_id,
"status": "ALREADY_IMPORTED",
"result_sha256": result["result_sha256"],
}
self._validate_job_contract(job, result)
job_handshakes = self._load_job_handshakes(job_id)
unique_hashes = {
row["hash22000"]
for row in job_handshakes
}
if len(unique_hashes) != job["hash_count"]:
raise JobResultError(
"Job hash_count does not match unique "
"crack_job_handshakes hash22000 values"
)
found = self._parse_show_file(
show_path,
job_handshakes,
)
credential_stats = self._import_credentials(
found,
job_handshakes,
)
attempt_count = self._register_attempts(
job,
result,
job_handshakes,
)
self._update_steps(job_id, result["steps"])
released = self._release_reservations(job_id)
imported_at = self._now()
self._jobs.update_job_status(
job_id,
"COMPLETED",
)
self._jobs.update_job_result(
job_id,
result_sha256=result["result_sha256"],
imported_at=imported_at,
)
self._results.create_result(
job_id,
"COMPLETED",
result,
imported_at=imported_at,
)
self._audit.create_event(
event_type="JOB_RESULT_IMPORTED",
job_id=job_id,
client_id=job["client_id"],
method_id=job["method_id"],
method_version=job["method_version"],
details={
"result_sha256": result["result_sha256"],
"credentials_imported": credential_stats["imported"],
"credentials_already_present": (
credential_stats["already_present"]
),
"attempts_completed": attempt_count,
"reservations_released": released,
},
)
self._conn.commit()
return {
"job_id": job_id,
"status": "COMPLETED",
"result_sha256": result["result_sha256"],
"credentials_imported": credential_stats["imported"],
"credentials_already_present": (
credential_stats["already_present"]
),
"attempts_completed": attempt_count,
"reservations_released": released,
}
except Exception:
self._conn.rollback()
raise
@staticmethod
def _now() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat(
timespec="milliseconds"
).replace("+00:00", "Z")
@staticmethod
def _read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise JobResultError(
f"Invalid result.json: {path}"
) from exc
if not isinstance(value, dict):
raise JobResultError("result.json must contain a JSON object")
return value
@staticmethod
def _safe_extract(
zf: zipfile.ZipFile,
destination: Path,
) -> None:
destination = destination.resolve()
for member in zf.infolist():
target = (destination / member.filename).resolve()
try:
target.relative_to(destination)
except ValueError as exc:
raise JobResultError(
f"Unsafe path in result package: {member.filename}"
) from exc
if member.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with zf.open(member) as src, target.open("wb") as dst:
while True:
chunk = src.read(1024 * 1024)
if not chunk:
break
dst.write(chunk)
@staticmethod
def _find_result_files(
root: Path,
) -> tuple[Path, Path]:
manifests = list(root.rglob("result.json"))
if len(manifests) != 1:
raise JobResultError(
"Result package must contain exactly one result.json"
)
show_files = list(root.rglob("hashcat-show.txt"))
if len(show_files) != 1:
raise JobResultError(
"Result package must contain exactly one hashcat-show.txt"
)
return manifests[0], show_files[0]
@staticmethod
def _validate_result_sha256(result: dict[str, Any]) -> None:
expected = result.get("result_sha256")
if not isinstance(expected, str) or not expected:
raise JobResultError(
"Result is missing result_sha256"
)
payload = dict(result)
payload.pop("result_sha256", None)
payload_bytes = json.dumps(
payload,
ensure_ascii=False,
indent=2,
).encode("utf-8")
actual = hashlib.sha256(payload_bytes).hexdigest()
if actual != expected:
raise JobResultError(
"Invalid result_sha256: "
f"expected {expected}, calculated {actual}"
)
def _check_idempotency(
self,
job,
result: dict[str, Any],
) -> bool:
result_sha256 = result["result_sha256"]
stored_job_sha = job["result_sha256"]
stored_result = self._results.get_result(job["id"])
if stored_job_sha is not None:
if stored_job_sha != result_sha256:
raise JobResultError(
f"Job {job['id']} already has a different result_sha256"
)
return True
if stored_result is not None:
stored_payload = self._results.decode_result(stored_result)
stored_sha = stored_payload.get("result_sha256")
if stored_sha != result_sha256:
raise JobResultError(
f"Job {job['id']} already has a different imported result"
)
return True
if job["imported_at"] is not None:
raise JobResultError(
f"Job {job['id']} is marked imported without a stored result"
)
if job["status"] == "COMPLETED":
raise JobResultError(
f"Job {job['id']} is already COMPLETED without an imported result"
)
return False
def _validate_job_contract(
self,
job,
result: dict[str, Any],
) -> None:
if result.get("schema_version") != 1:
raise JobResultError(
"Unsupported result schema_version: "
f"{result.get('schema_version')!r}"
)
if result.get("job_id") != job["id"]:
raise JobResultError(
"Result Job ID does not match the database Job"
)
if result.get("status") != "COMPLETED":
raise JobResultError(
"Only COMPLETED results can be imported"
)
if result.get("hash_file_sha256") != job["hash_file_sha256"]:
raise JobResultError(
"Result hash_file_sha256 does not match the Job"
)
if result.get("hash_count") != job["hash_count"]:
raise JobResultError(
"Result hash_count does not match the Job"
)
method = result.get("method")
if not isinstance(method, dict):
raise JobResultError(
"Result is missing method object"
)
if method.get("id") != job["method_id"]:
raise JobResultError(
"Result method id does not match the Job"
)
if method.get("version") != job["method_version"]:
raise JobResultError(
"Result method version does not match the Job"
)
completed_at = result.get("completed_at")
if not isinstance(completed_at, str) or not completed_at:
raise JobResultError(
"Result is missing completed_at"
)
result_steps = result.get("steps")
if not isinstance(result_steps, list):
raise JobResultError(
"Result is missing steps list"
)
if len(result_steps) != job["step_count"]:
raise JobResultError(
"Result step count does not match the Job"
)
db_steps = self._jobs.list_job_steps(job["id"])
if len(db_steps) != job["step_count"]:
raise JobResultError(
"Database Job step count does not match crack_job_steps"
)
db_by_no = {
row["step_no"]: row
for row in db_steps
}
seen_step_numbers: set[int] = set()
for step in result_steps:
if not isinstance(step, dict):
raise JobResultError(
"Each result step must be an object"
)
step_no = step.get("step_no")
if not isinstance(step_no, int):
raise JobResultError(
f"Invalid result step_no: {step_no!r}"
)
if step_no in seen_step_numbers:
raise JobResultError(
f"Duplicate result step_no: {step_no}"
)
seen_step_numbers.add(step_no)
db_step = db_by_no.get(step_no)
if db_step is None:
raise JobResultError(
f"Result contains unknown step_no: {step_no}"
)
if step.get("step_id") != db_step["step_id"]:
raise JobResultError(
f"Step {step_no}: step_id does not match the Job"
)
if step.get("session") != db_step["session_name"]:
raise JobResultError(
f"Step {step_no}: session does not match the Job"
)
if step.get("status") != "COMPLETED":
raise JobResultError(
f"Step {step_no}: result status must be COMPLETED"
)
def _load_job_handshakes(
self,
job_id: str,
) -> list[dict[str, Any]]:
rows = self._jobs.list_job_handshakes(job_id)
if not rows:
raise JobResultError(
f"Job {job_id} contains no handshakes"
)
result = []
for row in rows:
result.append(
{
"handshake_id": row["handshake_id"],
"hash22000": row["hash22000"],
"access_point_id": row["access_point_id"],
}
)
return result
@staticmethod
def _normalize_mac(value: str) -> str:
normalized = value.strip().lower().replace(":", "").replace("-", "")
if len(normalized) != 12:
raise JobResultError(
f"Invalid MAC address: {value!r}"
)
try:
int(normalized, 16)
except ValueError as exc:
raise JobResultError(
f"Invalid MAC address: {value!r}"
) from exc
return normalized
@classmethod
def _hash_identity(
cls,
hash22000: str,
) -> tuple[str, str, str]:
parts = hash22000.split("*")
if len(parts) < 6 or parts[0] != "WPA":
raise JobResultError(
f"Unsupported hash22000 format: {hash22000}"
)
if parts[1] not in {"01", "02"}:
raise JobResultError(
f"Unsupported WPA hash type: {parts[1]}"
)
hash_value = parts[2].strip().lower()
ap_mac = cls._normalize_mac(parts[3])
client_mac = cls._normalize_mac(parts[4])
if not hash_value:
raise JobResultError(
f"Invalid hash22000 hash value: {hash22000}"
)
return (
hash_value,
ap_mac,
client_mac,
)
@classmethod
def _parse_show_identity(
cls,
line: str,
) -> tuple[str, str, str, str]:
fields = line.split(":", 4)
if len(fields) != 5:
raise JobResultError(
f"Invalid Hashcat --show line: {line}"
)
hash_value = fields[0].strip().lower()
ap_mac = cls._normalize_mac(fields[1])
client_mac = cls._normalize_mac(fields[2])
password = fields[4]
if not hash_value:
raise JobResultError(
f"Invalid Hashcat --show hash: {line}"
)
return (
hash_value,
ap_mac,
client_mac,
password,
)
def _parse_show_file(
self,
path: Path,
job_handshakes: list[dict[str, Any]],
) -> list[tuple[str, str]]:
identity_to_hashes: dict[
tuple[str, str, str],
list[str],
] = defaultdict(list)
for row in job_handshakes:
identity = self._hash_identity(row["hash22000"])
hashes = identity_to_hashes[identity]
if row["hash22000"] not in hashes:
hashes.append(row["hash22000"])
found: list[tuple[str, str]] = []
seen: set[tuple[str, str]] = set()
for line_no, raw_line in enumerate(
path.read_text(encoding="utf-8").splitlines(),
start=1,
):
line = raw_line.strip()
if not line:
continue
hash_value, ap_mac, client_mac, password = (
self._parse_show_identity(line)
)
identity = (
hash_value,
ap_mac,
client_mac,
)
hashes = identity_to_hashes.get(identity)
if not hashes:
raise JobResultError(
"Result contains an unknown Hashcat --show line "
f"{line_no}: {line}"
)
for hash22000 in hashes:
item = (hash22000, password)
if item not in seen:
seen.add(item)
found.append(item)
return found
def _import_credentials(
self,
found: list[tuple[str, str]],
job_handshakes: list[dict[str, Any]],
) -> dict[str, int]:
by_hash: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in job_handshakes:
by_hash[row["hash22000"]].append(row)
imported = 0
already_present = 0
for hash22000, password in found:
mappings = by_hash.get(hash22000)
if not mappings:
raise JobResultError(
"Hashcat --show contains a hash that is not part "
f"of the Job: {hash22000}"
)
for mapping in mappings:
handshake_id = mapping["handshake_id"]
access_point_id = mapping["access_point_id"]
if access_point_id is None:
raise JobResultError(
"Cannot import credential for handshake "
f"{handshake_id}: access_point_id is NULL"
)
existing = self._credentials.get_credential(
access_point_id,
handshake_id,
password,
)
if existing is not None:
already_present += 1
self._credentials.mark_access_point_cracked(
access_point_id,
)
continue
self._credentials.create_hashcat_credential(
access_point_id,
handshake_id,
password,
)
imported += 1
return {
"imported": imported,
"already_present": already_present,
}
def _register_attempts(
self,
job,
result: dict[str, Any],
job_handshakes: list[dict[str, Any]],
) -> int:
if result["status"] != "COMPLETED":
return 0
completed_at = result["completed_at"]
count = 0
for row in job_handshakes:
existing = self._attempts.get_attempt(
row["handshake_id"],
job["method_id"],
job["method_version"],
)
if existing is not None:
if (
existing["job_id"] != job["id"]
or existing["status"] != "COMPLETED"
):
raise JobResultError(
"Handshake "
f"{row['handshake_id']} already has a "
"conflicting attempt for this method/version"
)
continue
self._attempts.create_attempt(
handshake_id=row["handshake_id"],
method_id=job["method_id"],
method_version=job["method_version"],
job_id=job["id"],
completed_at=completed_at,
)
count += 1
return count
def _update_steps(
self,
job_id: str,
result_steps: list[dict[str, Any]],
) -> None:
for step in result_steps:
step_no = step["step_no"]
self._jobs.update_job_step(
job_id=job_id,
step_no=step_no,
status="COMPLETED",
started_at=step.get("started_at"),
completed_at=step.get("completed_at"),
exit_code=step.get("exit_code"),
restore_seen=(
1 if step.get("restore_seen") else 0
),
error=step.get("error"),
)
updated = self._jobs.get_job_step(
job_id,
step_no,
)
if updated is None:
raise JobResultError(
f"Job step disappeared during result import: "
f"{step_no}"
)
def _release_reservations(
self,
job_id: str,
) -> int:
released = 0
for reservation in self._reservations.list_job_reservations(
job_id
):
if reservation["status"] != "ACTIVE":
continue
self._reservations.update_status(
reservation["id"],
"RELEASED",
)
released += 1
return released
@@ -0,0 +1,251 @@
from __future__ import annotations
import re
from typing import Any
class MethodDefinitionError(ValueError):
pass
_RESOURCE_RE = re.compile(r"^[a-z][a-z0-9_-]*:[A-Za-z0-9][A-Za-z0-9._-]*$")
_STEP_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
_MASK_CHARSET_SIZES = {
"?l": 26,
"?u": 26,
"?d": 10,
"?s": 33,
"?a": 95,
"?b": 256,
}
_MASK_MIN_LENGTH = 8
def validate_method_definition(
definition: dict[str, Any],
) -> dict[str, Any]:
if not isinstance(definition, dict):
raise MethodDefinitionError(
"Method definition must be a JSON object"
)
if definition.get("schema_version") != 1:
raise MethodDefinitionError(
"Unsupported method definition schema version"
)
hash_mode = definition.get("hash_mode")
if not isinstance(hash_mode, int) or isinstance(hash_mode, bool):
raise MethodDefinitionError(
"Method definition hash_mode must be a non-negative integer"
)
if hash_mode < 0:
raise MethodDefinitionError(
"Method definition hash_mode must be a non-negative integer"
)
steps = definition.get("steps")
if not isinstance(steps, list) or not steps:
raise MethodDefinitionError(
"Method definition must contain a non-empty steps list"
)
seen_step_ids: set[str] = set()
for step in steps:
_validate_step(step, seen_step_ids)
return definition
def _validate_step(
step: Any,
seen_step_ids: set[str],
) -> None:
if not isinstance(step, dict):
raise MethodDefinitionError(
"Each method step must be a JSON object"
)
step_id = step.get("step_id")
if not isinstance(step_id, str) or not _STEP_ID_RE.fullmatch(step_id):
raise MethodDefinitionError(
"Each method step must have a valid step_id"
)
if step_id in seen_step_ids:
raise MethodDefinitionError(
f"Duplicate step_id: {step_id}"
)
seen_step_ids.add(step_id)
attack_mode = step.get("attack_mode")
if not isinstance(attack_mode, int) or isinstance(attack_mode, bool):
raise MethodDefinitionError(
f"Invalid attack_mode for step: {step_id}"
)
if attack_mode < 0:
raise MethodDefinitionError(
f"Invalid attack_mode for step: {step_id}"
)
if attack_mode == 0:
_require_dictionary(step, step_id)
elif attack_mode == 1:
_require_dictionaries(step, step_id)
elif attack_mode == 3:
_require_mask(
step,
step_id,
minimum_length=_MASK_MIN_LENGTH,
)
elif attack_mode == 6:
_require_dictionary(step, step_id)
_require_mask(
step,
step_id,
minimum_length=_MASK_MIN_LENGTH,
)
elif attack_mode == 7:
_require_mask(
step,
step_id,
minimum_length=_MASK_MIN_LENGTH,
)
_require_dictionary(step, step_id)
if "rules" in step:
_validate_resource_object(
step["rules"],
"rules",
step_id,
)
def _require_dictionary(
step: dict[str, Any],
step_id: str,
) -> None:
if "dictionary" not in step:
raise MethodDefinitionError(
f"Step {step_id} requires dictionary"
)
_validate_resource_object(
step["dictionary"],
"dictionary",
step_id,
)
def _require_dictionaries(
step: dict[str, Any],
step_id: str,
) -> None:
dictionaries = step.get("dictionaries")
if not isinstance(dictionaries, list) or len(dictionaries) != 2:
raise MethodDefinitionError(
f"Step {step_id} requires exactly two dictionaries"
)
for dictionary in dictionaries:
_validate_resource_object(
dictionary,
"dictionary",
step_id,
)
def _require_mask(
step: dict[str, Any],
step_id: str,
minimum_length: int | None = None,
) -> None:
mask = step.get("mask")
if not isinstance(mask, str) or not mask:
raise MethodDefinitionError(
f"Step {step_id} requires a non-empty mask"
)
length = 0
index = 0
while index < len(mask):
if mask[index] != "?":
length += 1
index += 1
continue
if index + 1 >= len(mask):
raise MethodDefinitionError(
f"Step {step_id} mask contains a lone '?' "
f"at position {index + 1}"
)
token = mask[index:index + 2]
if token == "??":
length += 1
index += 2
continue
if token not in _MASK_CHARSET_SIZES:
raise MethodDefinitionError(
f"Step {step_id} contains unsupported mask token "
f"{token!r} at position {index + 1}"
)
length += 1
index += 2
if (
minimum_length is not None
and length < minimum_length
):
raise MethodDefinitionError(
f"Step {step_id} mask must produce at least "
f"{minimum_length} characters; got {length}"
)
def _validate_resource_object(
value: Any,
resource_type: str,
step_id: str,
) -> None:
if not isinstance(value, dict):
raise MethodDefinitionError(
f"Step {step_id} {resource_type} must be an object"
)
resource = value.get("resource")
if not isinstance(resource, str) or not _RESOURCE_RE.fullmatch(resource):
raise MethodDefinitionError(
f"Step {step_id} {resource_type}.resource must be "
"a valid logical resource ID"
)
expected_prefix = (
"wordlist:"
if resource_type == "dictionary"
else f"{resource_type}:"
)
if not resource.startswith(expected_prefix):
raise MethodDefinitionError(
f"Step {step_id} {resource_type}.resource must start "
f"with {expected_prefix!r}"
)
+118
View File
@@ -0,0 +1,118 @@
from __future__ import annotations
import sqlite3
import uuid
from typing import Any
from cracklab.server.repositories.methods import MethodsRepository
from cracklab.server.services.method_definition import (
MethodDefinitionError,
validate_method_definition,
)
class MethodServiceError(ValueError):
pass
class MethodService:
def __init__(self, conn: sqlite3.Connection) -> None:
self._methods = MethodsRepository(conn)
def create_method(
self,
name: str,
description: str | None = None,
method_id: str | None = None,
) -> str:
if not name or not name.strip():
raise MethodServiceError("Method name must not be empty")
if method_id is None:
method_id = f"MTH-{uuid.uuid4().hex}"
if not method_id:
raise MethodServiceError("Method ID must not be empty")
self._methods.create_method(
method_id=method_id,
name=name.strip(),
description=description.strip() if description else None,
)
return method_id
def create_method_version(
self,
method_id: str,
version: int,
definition: dict[str, Any],
) -> None:
if version <= 0:
raise MethodServiceError(
"Method version must be greater than zero"
)
try:
validate_method_definition(definition)
except MethodDefinitionError as exc:
raise MethodServiceError(str(exc)) from exc
self._methods.create_method_version(
method_id=method_id,
version=version,
definition=definition,
)
def create_next_method_version(
self,
method_id: str,
definition: dict[str, Any],
) -> int:
try:
validate_method_definition(definition)
except MethodDefinitionError as exc:
raise MethodServiceError(str(exc)) from exc
version = self._methods.get_next_method_version(method_id)
self._methods.create_method_version(
method_id=method_id,
version=version,
definition=definition,
)
return version
def get_method(self, method_id: str) -> sqlite3.Row | None:
return self._methods.get_method(method_id)
def list_methods(self) -> list[sqlite3.Row]:
return self._methods.list_methods()
def get_method_version(
self,
method_id: str,
version: int,
) -> sqlite3.Row | None:
return self._methods.get_method_version(
method_id,
version,
)
def list_method_versions(
self,
method_id: str,
) -> list[sqlite3.Row]:
return self._methods.list_method_versions(method_id)
def get_latest_method_version(
self,
method_id: str,
) -> sqlite3.Row | None:
versions = self._methods.list_method_versions(method_id)
if not versions:
return None
return versions[-1]
+914
View File
@@ -0,0 +1,914 @@
from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from typing import Any
from cracklab.server.services.methods import MethodService
@dataclass(frozen=True)
class StockMethod:
method_id: str
name: str
description: str
definition: dict[str, Any] | None
STOCK_METHODS = (
StockMethod(
method_id="mobile_oper_spb_plus_79",
name="MobileOperSpb+79",
description="Перебор мобильных номеров Санкт-Петербурга с префиксом +79.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"+791100?d?d?d?d?d",
"+791101?d?d?d?d?d",
"+791102?d?d?d?d?d",
"+791103?d?d?d?d?d",
"+791108?d?d?d?d?d",
"+791109?d?d?d?d?d",
"+791110?d?d?d?d?d",
"+791111?d?d?d?d?d",
"+791112?d?d?d?d?d",
"+791117?d?d?d?d?d",
"+791119?d?d?d?d?d",
"+79811?d?d?d?d?d?d",
"+79817?d?d?d?d?d?d",
"+79818?d?d?d?d?d?d",
"+79819?d?d?d?d?d?d",
"+798986?d?d?d?d?d",
"+798686?d?d?d?d?d",
"+798687?d?d?d?d?d",
"+798688?d?d?d?d?d",
"+792118?d?d?d?d?d",
"+792119?d?d?d?d?d",
"+79213?d?d?d?d?d?d",
"+79214?d?d?d?d?d?d",
"+79215?d?d?d?d?d?d",
"+79216?d?d?d?d?d?d",
"+79217?d?d?d?d?d?d",
"+79218?d?d?d?d?d?d",
"+79219?d?d?d?d?d?d",
"+793110?d?d?d?d?d",
"+793111?d?d?d?d?d",
"+793112?d?d?d?d?d",
"+793113?d?d?d?d?d",
"+79312?d?d?d?d?d?d",
"+79313?d?d?d?d?d?d",
"+79315?d?d?d?d?d?d",
"+79319?d?d?d?d?d?d",
"+79291?d?d?d?d?d?d",
"+799923?d?d?d?d?d",
"+799924?d?d?d?d?d",
"+799925?d?d?d?d?d",
"+7963241?d?d?d?d",
"+7963242?d?d?d?d",
"+7963243?d?d?d?d",
"+7963244?d?d?d?d",
"+7963245?d?d?d?d",
"+7963246?d?d?d?d",
"+7963247?d?d?d?d",
"+7963248?d?d?d?d",
"+7963249?d?d?d?d",
"+796330?d?d?d?d?d",
"+796331?d?d?d?d?d",
"+796332?d?d?d?d?d",
"+796334?d?d?d?d?d",
"+79650?d?d?d?d?d?d",
"+79651?d?d?d?d?d?d",
"+79652?d?d?d?d?d?d",
"+79653?d?d?d?d?d?d",
"+79654?d?d?d?d?d?d",
"+79655?d?d?d?d?d?d",
"+79656?d?d?d?d?d?d",
"+79657?d?d?d?d?d?d",
"+79658?d?d?d?d?d?d",
"+79659?d?d?d?d?d?d",
"+790520?d?d?d?d?d",
"+790521?d?d?d?d?d",
"+790522?d?d?d?d?d",
"+790523?d?d?d?d?d",
"+790524?d?d?d?d?d",
"+790525?d?d?d?d?d",
"+790526?d?d?d?d?d",
"+790527?d?d?d?d?d",
"+790528?d?d?d?d?d",
"+790624?d?d?d?d?d",
"+790625?d?d?d?d?d",
"+790626?d?d?d?d?d",
"+790627?d?d?d?d?d",
"+790958?d?d?d?d?d",
"+790959?d?d?d?d?d",
"+796023?d?d?d?d?d",
"+796024?d?d?d?d?d",
"+796025?d?d?d?d?d",
"+796026?d?d?d?d?d",
"+796027?d?d?d?d?d",
"+796028?d?d?d?d?d",
"+796434?d?d?d?d?d",
"+79500?d?d?d?d?d?d",
"+79502?d?d?d?d?d?d",
"+79516?d?d?d?d?d?d",
"+79517?d?d?d?d?d?d",
"+79518?d?d?d?d?d?d",
"+79522?d?d?d?d?d?d",
"+79523?d?d?d?d?d?d",
"+79531?d?d?d?d?d?d",
"+79533?d?d?d?d?d?d",
"+790433?d?d?d?d?d",
"+790451?d?d?d?d?d",
"+790455?d?d?d?d?d",
"+790460?d?d?d?d?d",
"+790461?d?d?d?d?d",
"+790462?d?d?d?d?d",
"+790463?d?d?d?d?d",
"+790464?d?d?d?d?d",
"+799100?d?d?d?d?d",
"+799101?d?d?d?d?d",
"+799102?d?d?d?d?d",
"+790062?d?d?d?d?d",
"+790063?d?d?d?d?d",
"+790064?d?d?d?d?d",
"+790065?d?d?d?d?d",
"+799900?d?d?d?d?d",
"+799901?d?d?d?d?d",
"+799902?d?d?d?d?d",
"+799903?d?d?d?d?d",
"+799904?d?d?d?d?d",
"+799909?d?d?d?d?d",
"+79991?d?d?d?d?d?d",
"+79992?d?d?d?d?d?d",
"+799322?d?d?d?d?d",
"+799343?d?d?d?d?d",
"+799344?d?d?d?d?d",
"+799555?d?d?d?d?d",
"+799559?d?d?d?d?d",
"+799560?d?d?d?d?d",
"+799590?d?d?d?d?d",
"+795840?d?d?d?d?d",
"+795856?d?d?d?d?d",
"+799511?d?d?d?d?d",
"+799512?d?d?d?d?d",
"+7999944?d?d?d?d",
"+7999945?d?d?d?d",
"+7999946?d?d?d?d",
"+7999947?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_spb_89",
name="MobileOperSpb_89",
description="Перебор мобильных номеров Санкт-Петербурга с префиксом 89.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"891100?d?d?d?d?d",
"891101?d?d?d?d?d",
"891102?d?d?d?d?d",
"891103?d?d?d?d?d",
"891108?d?d?d?d?d",
"891109?d?d?d?d?d",
"891110?d?d?d?d?d",
"891111?d?d?d?d?d",
"891112?d?d?d?d?d",
"891117?d?d?d?d?d",
"891119?d?d?d?d?d",
"89811?d?d?d?d?d?d",
"89817?d?d?d?d?d?d",
"89818?d?d?d?d?d?d",
"89819?d?d?d?d?d?d",
"898986?d?d?d?d?d",
"898686?d?d?d?d?d",
"898687?d?d?d?d?d",
"898688?d?d?d?d?d",
"892118?d?d?d?d?d",
"892119?d?d?d?d?d",
"89213?d?d?d?d?d?d",
"89214?d?d?d?d?d?d",
"89215?d?d?d?d?d?d",
"89216?d?d?d?d?d?d",
"89217?d?d?d?d?d?d",
"89218?d?d?d?d?d?d",
"89219?d?d?d?d?d?d",
"893110?d?d?d?d?d",
"893111?d?d?d?d?d",
"893112?d?d?d?d?d",
"893113?d?d?d?d?d",
"89312?d?d?d?d?d?d",
"89313?d?d?d?d?d?d",
"89315?d?d?d?d?d?d",
"89319?d?d?d?d?d?d",
"89291?d?d?d?d?d?d",
"899923?d?d?d?d?d",
"899924?d?d?d?d?d",
"899925?d?d?d?d?d",
"8963241?d?d?d?d",
"8963242?d?d?d?d",
"8963243?d?d?d?d",
"8963244?d?d?d?d",
"8963245?d?d?d?d",
"8963246?d?d?d?d",
"8963247?d?d?d?d",
"8963248?d?d?d?d",
"8963249?d?d?d?d",
"896330?d?d?d?d?d",
"896331?d?d?d?d?d",
"896332?d?d?d?d?d",
"896334?d?d?d?d?d",
"89650?d?d?d?d?d?d",
"89651?d?d?d?d?d?d",
"89652?d?d?d?d?d?d",
"89653?d?d?d?d?d?d",
"89654?d?d?d?d?d?d",
"89655?d?d?d?d?d?d",
"89656?d?d?d?d?d?d",
"89657?d?d?d?d?d?d",
"89658?d?d?d?d?d?d",
"89659?d?d?d?d?d?d",
"890520?d?d?d?d?d",
"890521?d?d?d?d?d",
"890522?d?d?d?d?d",
"890523?d?d?d?d?d",
"890524?d?d?d?d?d",
"890525?d?d?d?d?d",
"890526?d?d?d?d?d",
"890527?d?d?d?d?d",
"890528?d?d?d?d?d",
"890624?d?d?d?d?d",
"890625?d?d?d?d?d",
"890626?d?d?d?d?d",
"890627?d?d?d?d?d",
"890958?d?d?d?d?d",
"890959?d?d?d?d?d",
"896023?d?d?d?d?d",
"896024?d?d?d?d?d",
"896025?d?d?d?d?d",
"896026?d?d?d?d?d",
"896027?d?d?d?d?d",
"896028?d?d?d?d?d",
"896434?d?d?d?d?d",
"89500?d?d?d?d?d?d",
"89502?d?d?d?d?d?d",
"89516?d?d?d?d?d?d",
"89517?d?d?d?d?d?d",
"89518?d?d?d?d?d?d",
"89522?d?d?d?d?d?d",
"89523?d?d?d?d?d?d",
"89531?d?d?d?d?d?d",
"89533?d?d?d?d?d?d",
"890433?d?d?d?d?d",
"890451?d?d?d?d?d",
"890455?d?d?d?d?d",
"890460?d?d?d?d?d",
"890461?d?d?d?d?d",
"890462?d?d?d?d?d",
"890463?d?d?d?d?d",
"890464?d?d?d?d?d",
"899100?d?d?d?d?d",
"899101?d?d?d?d?d",
"899102?d?d?d?d?d",
"890062?d?d?d?d?d",
"890063?d?d?d?d?d",
"890064?d?d?d?d?d",
"890065?d?d?d?d?d",
"899900?d?d?d?d?d",
"899901?d?d?d?d?d",
"899902?d?d?d?d?d",
"899903?d?d?d?d?d",
"899904?d?d?d?d?d",
"899909?d?d?d?d?d",
"89991?d?d?d?d?d?d",
"89992?d?d?d?d?d?d",
"899322?d?d?d?d?d",
"899343?d?d?d?d?d",
"899344?d?d?d?d?d",
"899555?d?d?d?d?d",
"899559?d?d?d?d?d",
"899560?d?d?d?d?d",
"899590?d?d?d?d?d",
"895840?d?d?d?d?d",
"895856?d?d?d?d?d",
"899511?d?d?d?d?d",
"899512?d?d?d?d?d",
"8999944?d?d?d?d",
"8999945?d?d?d?d",
"8999946?d?d?d?d",
"8999947?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_spb_79",
name="MobileOperSpb_79",
description="Перебор мобильных номеров Санкт-Петербурга без знака +.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"791100?d?d?d?d?d",
"791101?d?d?d?d?d",
"791102?d?d?d?d?d",
"791103?d?d?d?d?d",
"791108?d?d?d?d?d",
"791109?d?d?d?d?d",
"791110?d?d?d?d?d",
"791111?d?d?d?d?d",
"791112?d?d?d?d?d",
"791117?d?d?d?d?d",
"791119?d?d?d?d?d",
"79811?d?d?d?d?d?d",
"79817?d?d?d?d?d?d",
"79818?d?d?d?d?d?d",
"79819?d?d?d?d?d?d",
"798986?d?d?d?d?d",
"798686?d?d?d?d?d",
"798687?d?d?d?d?d",
"798688?d?d?d?d?d",
"792118?d?d?d?d?d",
"792119?d?d?d?d?d",
"79213?d?d?d?d?d?d",
"79214?d?d?d?d?d?d",
"79215?d?d?d?d?d?d",
"79216?d?d?d?d?d?d",
"79217?d?d?d?d?d?d",
"79218?d?d?d?d?d?d",
"79219?d?d?d?d?d?d",
"793110?d?d?d?d?d",
"793111?d?d?d?d?d",
"793112?d?d?d?d?d",
"793113?d?d?d?d?d",
"79312?d?d?d?d?d?d",
"79313?d?d?d?d?d?d",
"79315?d?d?d?d?d?d",
"79319?d?d?d?d?d?d",
"79291?d?d?d?d?d?d",
"799923?d?d?d?d?d",
"799924?d?d?d?d?d",
"799925?d?d?d?d?d",
"7963241?d?d?d?d",
"7963242?d?d?d?d",
"7963243?d?d?d?d",
"7963244?d?d?d?d",
"7963245?d?d?d?d",
"7963246?d?d?d?d",
"7963247?d?d?d?d",
"7963248?d?d?d?d",
"7963249?d?d?d?d",
"796330?d?d?d?d?d",
"796331?d?d?d?d?d",
"796332?d?d?d?d?d",
"796334?d?d?d?d?d",
"79650?d?d?d?d?d?d",
"79651?d?d?d?d?d?d",
"79652?d?d?d?d?d?d",
"79653?d?d?d?d?d?d",
"79654?d?d?d?d?d?d",
"79655?d?d?d?d?d?d",
"79656?d?d?d?d?d?d",
"79657?d?d?d?d?d?d",
"79658?d?d?d?d?d?d",
"79659?d?d?d?d?d?d",
"790520?d?d?d?d?d",
"790521?d?d?d?d?d",
"790522?d?d?d?d?d",
"790523?d?d?d?d?d",
"790524?d?d?d?d?d",
"790525?d?d?d?d?d",
"790526?d?d?d?d?d",
"790527?d?d?d?d?d",
"790528?d?d?d?d?d",
"790624?d?d?d?d?d",
"790625?d?d?d?d?d",
"790626?d?d?d?d?d",
"790627?d?d?d?d?d",
"790958?d?d?d?d?d",
"790959?d?d?d?d?d",
"796023?d?d?d?d?d",
"796024?d?d?d?d?d",
"796025?d?d?d?d?d",
"796026?d?d?d?d?d",
"796027?d?d?d?d?d",
"796028?d?d?d?d?d",
"796434?d?d?d?d?d",
"79500?d?d?d?d?d?d",
"79502?d?d?d?d?d?d",
"79516?d?d?d?d?d?d",
"79517?d?d?d?d?d?d",
"79518?d?d?d?d?d?d",
"79522?d?d?d?d?d?d",
"79523?d?d?d?d?d?d",
"79531?d?d?d?d?d?d",
"79533?d?d?d?d?d?d",
"790433?d?d?d?d?d",
"790451?d?d?d?d?d",
"790455?d?d?d?d?d",
"790460?d?d?d?d?d",
"790461?d?d?d?d?d",
"790462?d?d?d?d?d",
"790463?d?d?d?d?d",
"790464?d?d?d?d?d",
"799100?d?d?d?d?d",
"799101?d?d?d?d?d",
"799102?d?d?d?d?d",
"790062?d?d?d?d?d",
"790063?d?d?d?d?d",
"790064?d?d?d?d?d",
"790065?d?d?d?d?d",
"799900?d?d?d?d?d",
"799901?d?d?d?d?d",
"799902?d?d?d?d?d",
"799903?d?d?d?d?d",
"799904?d?d?d?d?d",
"799909?d?d?d?d?d",
"79991?d?d?d?d?d?d",
"79992?d?d?d?d?d?d",
"799322?d?d?d?d?d",
"799343?d?d?d?d?d",
"799344?d?d?d?d?d",
"799555?d?d?d?d?d",
"799559?d?d?d?d?d",
"799560?d?d?d?d?d",
"799590?d?d?d?d?d",
"795840?d?d?d?d?d",
"795856?d?d?d?d?d",
"799511?d?d?d?d?d",
"799512?d?d?d?d?d",
"7999944?d?d?d?d",
"7999945?d?d?d?d",
"7999946?d?d?d?d",
"7999947?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_spb_9",
name="MobileOperSpb_9",
description="Перебор мобильных номеров Санкт-Петербурга без 7 и знака +.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"91100?d?d?d?d?d",
"91101?d?d?d?d?d",
"91102?d?d?d?d?d",
"91103?d?d?d?d?d",
"91108?d?d?d?d?d",
"91109?d?d?d?d?d",
"91110?d?d?d?d?d",
"91111?d?d?d?d?d",
"91112?d?d?d?d?d",
"91117?d?d?d?d?d",
"91119?d?d?d?d?d",
"9811?d?d?d?d?d?d",
"9817?d?d?d?d?d?d",
"9818?d?d?d?d?d?d",
"9819?d?d?d?d?d?d",
"98986?d?d?d?d?d",
"98686?d?d?d?d?d",
"98687?d?d?d?d?d",
"98688?d?d?d?d?d",
"92118?d?d?d?d?d",
"92119?d?d?d?d?d",
"9213?d?d?d?d?d?d",
"9214?d?d?d?d?d?d",
"9215?d?d?d?d?d?d",
"9216?d?d?d?d?d?d",
"9217?d?d?d?d?d?d",
"9218?d?d?d?d?d?d",
"9219?d?d?d?d?d?d",
"93110?d?d?d?d?d",
"93111?d?d?d?d?d",
"93112?d?d?d?d?d",
"93113?d?d?d?d?d",
"9312?d?d?d?d?d?d",
"9313?d?d?d?d?d?d",
"9315?d?d?d?d?d?d",
"9319?d?d?d?d?d?d",
"9291?d?d?d?d?d?d",
"99923?d?d?d?d?d",
"99924?d?d?d?d?d",
"99925?d?d?d?d?d",
"963241?d?d?d?d",
"963242?d?d?d?d",
"963243?d?d?d?d",
"963244?d?d?d?d",
"963245?d?d?d?d",
"963246?d?d?d?d",
"963247?d?d?d?d",
"963248?d?d?d?d",
"963249?d?d?d?d",
"96330?d?d?d?d?d",
"96331?d?d?d?d?d",
"96332?d?d?d?d?d",
"96334?d?d?d?d?d",
"9650?d?d?d?d?d?d",
"9651?d?d?d?d?d?d",
"9652?d?d?d?d?d?d",
"9653?d?d?d?d?d?d",
"9654?d?d?d?d?d?d",
"9655?d?d?d?d?d?d",
"9656?d?d?d?d?d?d",
"9657?d?d?d?d?d?d",
"9658?d?d?d?d?d?d",
"9659?d?d?d?d?d?d",
"90520?d?d?d?d?d",
"90521?d?d?d?d?d",
"90522?d?d?d?d?d",
"90523?d?d?d?d?d",
"90524?d?d?d?d?d",
"90525?d?d?d?d?d",
"90526?d?d?d?d?d",
"90527?d?d?d?d?d",
"90528?d?d?d?d?d",
"90624?d?d?d?d?d",
"90625?d?d?d?d?d",
"90626?d?d?d?d?d",
"90627?d?d?d?d?d",
"90958?d?d?d?d?d",
"90959?d?d?d?d?d",
"96023?d?d?d?d?d",
"96024?d?d?d?d?d",
"96025?d?d?d?d?d",
"96026?d?d?d?d?d",
"96027?d?d?d?d?d",
"96028?d?d?d?d?d",
"96434?d?d?d?d?d",
"9500?d?d?d?d?d?d",
"9502?d?d?d?d?d?d",
"9516?d?d?d?d?d?d",
"9517?d?d?d?d?d?d",
"9518?d?d?d?d?d?d",
"9522?d?d?d?d?d?d",
"9523?d?d?d?d?d?d",
"9531?d?d?d?d?d?d",
"9533?d?d?d?d?d?d",
"90433?d?d?d?d?d",
"90451?d?d?d?d?d",
"90455?d?d?d?d?d",
"90460?d?d?d?d?d",
"90461?d?d?d?d?d",
"90462?d?d?d?d?d",
"90463?d?d?d?d?d",
"90464?d?d?d?d?d",
"99100?d?d?d?d?d",
"99101?d?d?d?d?d",
"99102?d?d?d?d?d",
"90062?d?d?d?d?d",
"90063?d?d?d?d?d",
"90064?d?d?d?d?d",
"90065?d?d?d?d?d",
"99900?d?d?d?d?d",
"99901?d?d?d?d?d",
"99902?d?d?d?d?d",
"99903?d?d?d?d?d",
"99904?d?d?d?d?d",
"99909?d?d?d?d?d",
"9991?d?d?d?d?d?d",
"9992?d?d?d?d?d?d",
"99322?d?d?d?d?d",
"99343?d?d?d?d?d",
"99344?d?d?d?d?d",
"99555?d?d?d?d?d",
"99559?d?d?d?d?d",
"99560?d?d?d?d?d",
"99590?d?d?d?d?d",
"95840?d?d?d?d?d",
"95856?d?d?d?d?d",
"99511?d?d?d?d?d",
"99512?d?d?d?d?d",
"999944?d?d?d?d",
"999945?d?d?d?d",
"999946?d?d?d?d",
"999947?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_plus_79",
name="MobileOper+79",
description="Перебор мобильных номеров с префиксом +79.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"+7911?d?d?d?d?d?d?d",
"+7904?d?d?d?d?d?d?d",
"+7931?d?d?d?d?d?d?d",
"+7921?d?d?d?d?d?d?d",
"+7999?d?d?d?d?d?d?d",
"+7905?d?d?d?d?d?d?d",
"+7951?d?d?d?d?d?d?d",
"+7952?d?d?d?d?d?d?d",
"+7953?d?d?d?d?d?d?d",
"+7901?d?d?d?d?d?d?d",
"+7903?d?d?d?d?d?d?d",
"+7950?d?d?d?d?d?d?d",
"+7906?d?d?d?d?d?d?d",
"+7909?d?d?d?d?d?d?d",
"+7929?d?d?d?d?d?d?d",
"+7960?d?d?d?d?d?d?d",
"+7961?d?d?d?d?d?d?d",
"+7962?d?d?d?d?d?d?d",
"+7963?d?d?d?d?d?d?d",
"+7965?d?d?d?d?d?d?d",
"+7966?d?d?d?d?d?d?d",
"+7981?d?d?d?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_89",
name="MobileOper_89",
description="Перебор мобильных номеров с префиксом 89.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"8911?d?d?d?d?d?d?d",
"8904?d?d?d?d?d?d?d",
"8931?d?d?d?d?d?d?d",
"8921?d?d?d?d?d?d?d",
"8999?d?d?d?d?d?d?d",
"8905?d?d?d?d?d?d?d",
"8951?d?d?d?d?d?d?d",
"8952?d?d?d?d?d?d?d",
"8953?d?d?d?d?d?d?d",
"8901?d?d?d?d?d?d?d",
"8903?d?d?d?d?d?d?d",
"8950?d?d?d?d?d?d?d",
"8906?d?d?d?d?d?d?d",
"8909?d?d?d?d?d?d?d",
"8929?d?d?d?d?d?d?d",
"8960?d?d?d?d?d?d?d",
"8961?d?d?d?d?d?d?d",
"8962?d?d?d?d?d?d?d",
"8963?d?d?d?d?d?d?d",
"8965?d?d?d?d?d?d?d",
"8966?d?d?d?d?d?d?d",
"8981?d?d?d?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_79",
name="MobileOper_79",
description="Перебор мобильных номеров без знака +.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"7911?d?d?d?d?d?d?d",
"7904?d?d?d?d?d?d?d",
"7931?d?d?d?d?d?d?d",
"7921?d?d?d?d?d?d?d",
"7999?d?d?d?d?d?d?d",
"7905?d?d?d?d?d?d?d",
"7951?d?d?d?d?d?d?d",
"7952?d?d?d?d?d?d?d",
"7953?d?d?d?d?d?d?d",
"7901?d?d?d?d?d?d?d",
"7903?d?d?d?d?d?d?d",
"7950?d?d?d?d?d?d?d",
"7906?d?d?d?d?d?d?d",
"7909?d?d?d?d?d?d?d",
"7929?d?d?d?d?d?d?d",
"7960?d?d?d?d?d?d?d",
"7961?d?d?d?d?d?d?d",
"7962?d?d?d?d?d?d?d",
"7963?d?d?d?d?d?d?d",
"7965?d?d?d?d?d?d?d",
"7966?d?d?d?d?d?d?d",
"7981?d?d?d?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mobile_oper_9",
name="MobileOper_9",
description="Перебор мобильных номеров без 7 и знака +.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": f"mask-{index:03d}",
"attack_mode": 3,
"mask": mask,
}
for index, mask in enumerate(
(
"911?d?d?d?d?d?d?d",
"904?d?d?d?d?d?d?d",
"931?d?d?d?d?d?d?d",
"921?d?d?d?d?d?d?d",
"999?d?d?d?d?d?d?d",
"905?d?d?d?d?d?d?d",
"951?d?d?d?d?d?d?d",
"952?d?d?d?d?d?d?d",
"953?d?d?d?d?d?d?d",
"901?d?d?d?d?d?d?d",
"903?d?d?d?d?d?d?d",
"950?d?d?d?d?d?d?d",
"906?d?d?d?d?d?d?d",
"909?d?d?d?d?d?d?d",
"929?d?d?d?d?d?d?d",
"960?d?d?d?d?d?d?d",
"961?d?d?d?d?d?d?d",
"962?d?d?d?d?d?d?d",
"963?d?d?d?d?d?d?d",
"965?d?d?d?d?d?d?d",
"966?d?d?d?d?d?d?d",
"981?d?d?d?d?d?d?d",
),
start=1,
)
],
},
),
StockMethod(
method_id="mask_8_digits",
name="8-digital",
description="Перебор восьмизначных числовых паролей.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": "mask-001",
"attack_mode": 3,
"mask": "?d?d?d?d?d?d?d?d",
},
],
},
),
StockMethod(
method_id="breachcompilation",
name="BreachCompilation",
description="Перебор по словарю BreachCompilation.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": "dictionary-001",
"attack_mode": 0,
"dictionary": {
"resource": "wordlist:breachcompilation",
},
},
],
},
),
StockMethod(
method_id="insidepro_wpa",
name="insidePRO_WPA",
description="Перебор по словарю insidePRO_WPA.",
definition={
"schema_version": 1,
"hash_mode": 22000,
"steps": [
{
"step_id": "dictionary-001",
"attack_mode": 0,
"dictionary": {
"resource": "wordlist:insidepro-wpa",
},
},
],
},
),
)
class StockMethodSeeder:
def __init__(self, conn: sqlite3.Connection) -> None:
self._conn = conn
self._methods = MethodService(conn)
def seed(self) -> None:
with self._conn:
for stock_method in STOCK_METHODS:
if stock_method.definition is None:
continue
existing_method = self._methods.get_method(
stock_method.method_id
)
if existing_method is None:
self._methods.create_method(
method_id=stock_method.method_id,
name=stock_method.name,
description=stock_method.description,
)
else:
if existing_method["name"] != stock_method.name:
raise ValueError(
"Stock method ID is already used with a "
f"different name: {stock_method.method_id}"
)
existing_version = self._methods.get_method_version(
stock_method.method_id,
1,
)
if existing_version is None:
self._methods.create_method_version(
method_id=stock_method.method_id,
version=1,
definition=stock_method.definition,
)
elif existing_version["definition_json"] != (
self._canonical_json(stock_method.definition)
):
raise ValueError(
"Stock method version 1 already exists with "
f"different definition: {stock_method.method_id}"
)
@staticmethod
def _canonical_json(definition: dict[str, Any]) -> str:
import json
return json.dumps(
definition,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
)