"""Data structures for memory dump analysis."""
from __future__ import annotations
import functools
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
if TYPE_CHECKING:
# Late import to avoid a cycle: dataset_metadata is a leaf module but
# keeping the import behind TYPE_CHECKING makes the dependency edge
# visible without forcing it at runtime.
from .dataset_metadata import DatasetMeta
[docs]
def deprecated_kwarg(cls, old_name: str, new_name: str):
"""Patch a dataclass __init__ to accept a deprecated kwarg name.
When callers pass *old_name* as a keyword argument, it is silently
mapped to *new_name* (unless *new_name* is also provided).
"""
orig_init = cls.__init__
@functools.wraps(orig_init)
def wrapper(self, *args, **kwargs):
if old_name in kwargs and new_name not in kwargs:
kwargs[new_name] = kwargs.pop(old_name)
orig_init(self, *args, **kwargs)
cls.__init__ = wrapper
[docs]
@dataclass
class CryptoSecret:
"""A parsed cryptographic secret from a keylog file."""
secret_type: str
identifier: bytes
secret_value: bytes
protocol: str = "TLS"
@property
def client_random(self) -> bytes:
"""Backward-compatible alias for identifier."""
return self.identifier
@client_random.setter
def client_random(self, value: bytes) -> None:
self.identifier = value
def __hash__(self):
return hash((self.secret_type, self.secret_value))
def __eq__(self, other):
if not isinstance(other, CryptoSecret):
return NotImplemented
return self.secret_type == other.secret_type and self.secret_value == other.secret_value
deprecated_kwarg(CryptoSecret, "client_random", "identifier")
# Backward-compatible alias
TLSSecret = CryptoSecret
[docs]
@dataclass
class KeyOccurrence:
"""A found key occurrence in a dump file."""
offset: int
secret: CryptoSecret
context_before: bytes
key_bytes: bytes
context_after: bytes
@property
def context_start_offset(self) -> int:
return self.offset - len(self.context_before)
[docs]
@dataclass
class DumpFile:
"""Metadata for a single dump file.
``kind`` tags the dump flavour so downstream code (discovery, API
responses, UI) can branch without re-sniffing magic bytes. Valid
values: ``"msl"``, ``"gdb_raw"``, ``"lldb_raw"``, ``"gcore"``,
``"raw"``.
"""
path: Path
timestamp: str
phase_prefix: str
phase_name: str
canonical_phase: Optional[str] = None
kind: str = "raw"
@property
def full_phase(self) -> str:
return f"{self.phase_prefix}_{self.phase_name}"
@property
def canonical_or_raw(self) -> str:
return self.canonical_phase if self.canonical_phase else self.full_phase
[docs]
@dataclass
class RunDirectory:
"""A single run directory containing dumps and keylog.
``meta`` is populated from ``meta.json`` when present (see
:func:`core.dataset_metadata.load_run_meta`). Legacy runs without a
``meta.json`` leave it ``None`` — discovery code must tolerate both.
"""
path: Path
library: str
protocol_version: str
run_number: int
dumps: List[DumpFile] = field(default_factory=list)
secrets: List[CryptoSecret] = field(default_factory=list)
secret_source: str = "none"
phase_mappings: Dict[str, str] = field(default_factory=dict)
meta: Optional["DatasetMeta"] = None
@property
def tls_version(self) -> str:
"""Backward-compatible alias for protocol_version."""
return self.protocol_version
@tls_version.setter
def tls_version(self, value: str) -> None:
self.protocol_version = value
[docs]
def get_dump_for_phase(self, phase: str) -> Optional[DumpFile]:
for d in self.dumps:
if d.full_phase == phase:
return d
return None
[docs]
def available_phases(self) -> List[str]:
return sorted(set(d.full_phase for d in self.dumps))
deprecated_kwarg(RunDirectory, "tls_version", "protocol_version")
[docs]
@dataclass
class ComparisonRegion:
"""Aligned regions across dumps for multi-run comparison."""
secret_type: str
key_length: int
context_size: int
run_data: List[Tuple[bytes, bytes, bytes]] = field(default_factory=list)
run_labels: List[str] = field(default_factory=list)
run_offsets: List[int] = field(default_factory=list)