Kara Zajac ad81964398
Build APK / build (push) Waiting to run
Implement rotation-clone presence engine (#1) + unit tests + CI tests
- detect/PresenceEngine.kt: identity-agnostic streaming detector for key-rotating clones (novel-churn gate, seamless-handover chain, co-movement gate, coherence). Full design in docs/detection-rotation-clone.md.
- Wired into ScanService as a parallel signal; CONFIRMED verdict raises a distinct rotating-tracker alert (softer tiers stay silent pending real-capture validation).
- PresenceEngineTest: synthetic clone (moving -> CONFIRMED), stationary clone (gated), single real tracker + far-ambient (CLEAR).
- CI now runs :app:testDebugUnitTest so the engine has behavioural verification, not just a compile check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 00:35:37 -04:00

VIGIL

What's been following you?

A native Android (Kotlin) app that watches for personal item-trackers — Apple AirTags, Tile, Samsung Galaxy SmartTags, and Google Find My Device / DULT tags — that are travelling with you over time. It is the temporal counterpart to OVERWATCH:

OVERWATCH is spatialwhat surveillance is watching this place, right now. VIGIL is temporalwhat has been with me, across time and places.

A tracker being near you means nothing — trackers are everywhere. The signal is persistence: the same device seen at many of your distinct locations, over a sustained window, while close enough to actually be on you. VIGIL is built around that idea.

⚠️ Prototype / work in progress. The scanning, parsing, temporal store, co-movement engine, allowlist, and learned baseline are in place; the app builds in CI. Wire-format offsets and thresholds are from the research brief and need validation against real hardware (see Status).


Privacy first — like OVERWATCH

VIGIL requests no INTERNET permission at all. There is no server, no account, no telemetry. Every tracker, every sighting, and the entire learned baseline live in an on-device SQLite database and never leave the phone. It listens only — it never transmits, probes, or interferes with any device.


What it detects

Four native BLE wire formats today, plus the emerging unified DULT format. VIGIL recognises each ecosystem and, where the ecosystem signals it, filters to the separated-from-owner state — the only state in which a following tracker is detectable (see the research brief).

Ecosystem BLE signature Separated-state signal Passive re-link window
Apple Find My / AirTag mfg data, company 0x004C, type 0x12 status byte "maintained" bit cleared ~24 h (key static per day)
Google Find My Device service data 0xFEAA, frame 0x40/0x41 frame 0x41 = separated (cleartext) ~24 h once separated
Samsung Galaxy SmartTag service data 0xFD5A state byte (lost / overmature-lost) ~24 h once overmature
Tile service data 0xFEED / 0xFEEC none — static MAC, always findable indefinite (static MAC)
DULT (unified, emerging) service data 0xFCB2 near-owner bit (byte 14 LSB) ~24 h separated

Chipolo, Pebblebee, eufy, Motorola, etc. inherit the signature of whichever network (Apple or Google) their SKU joined — VIGIL detects the network.

How it decides

Each parsed sighting is geotagged with a coarse fix and written to the temporal store. A tracker is escalated OBSERVED → SUSPICIOUS → ALERTING only when it clears the co-movement test:

  • ≥ 3 sightings (debounced to one per 15 min), across
  • ≥ N distinct places (geohash-7 cells; N = 2/3/4 by sensitivity), over
  • ≥ T minutes (30/45/90 by sensitivity), and
  • an RSSI proximity gate — it must have been genuinely close (on-body/in-bag) at least once. This is the piece AirGuard omits, and it rejects "a Tile in a passing car."

Two trust signals suppress false alarms:

  • Allowlist ("This is mine"). Tap a tracker to mark it approved — your own AirTag, your partner's Tile — and it never alerts again.
  • Learned offline baseline. VIGIL learns the places you dwell (home, work) as anchors, and a tracker seen at an anchor across several distinct days is auto-marked Known (home). So the household tags that are always around you fall silent on their own, entirely on-device.

The hard part — catching clones (problem #1)

Every shipping detector (AirGuard, iOS, Android's built-in) keys on device identity. A key-rotating clone (e.g. Positive Security's Find You: ~2,000 Find My keys, a new one every 30 s) looks like 2,000 one-off devices and evades all of them — it tracked a phone for 5 days with zero alerts.

VIGIL's headline goal is to detect the attack, not the device: a rotating clone is one physical radio holding an unbroken, close-range, co-moving RF presence even as its identity churns thousands of times faster than any standards-compliant tracker is allowed to. The full algorithm — a CUSUM churn trigger, an identity-agnostic presence-track confirmer, and a co-movement gate, plus the "identity-path × churn-path squeeze" that leaves no safe rotation rate — is designed in docs/detection-rotation-clone.md. It's now implemented as a first version in detect/PresenceEngine.kt (unit-tested against synthetic clone/ambient traces) and wired into the scan service to raise a distinct rotating-tracker alert; field-tuning against real captures is what remains.

Architecture

VIGIL reuses OVERWATCH's proven scanning stack and diverges where the temporal mission demands it (a persistent database instead of an in-memory store).

scan/TrackerSignatures.kt   BLE hex signatures for every ecosystem
scan/TrackerParser.kt       ScanResult -> TrackerObservation (four wire formats)
scan/BleTrackerScanner.kt   filtered BLE scan (screen-off capable) -> observations
service/ScanService.kt      foreground service; geotags + persists + alerts
data/db/VigilDatabase.kt    Room: trackers, sightings (14-day), baseline places
data/TrackerRepository.kt   ingest + baseline + evaluate on every sighting
detect/CoMovementEvaluator  the temporal co-movement test + RSSI proximity gate
detect/BaselineManager.kt   learns anchor places -> auto-trusts household tags
data/location/LocationProvider.kt   fused location (ported from OVERWATCH)
ui/ + MainActivity.kt       Compose UI (Catppuccin Mocha)

Paper

A full technical write-up — the per-ecosystem detection methodology (how VIGIL tracks each tracker type) and the harder modeling engine for what AirGuard and the built-in detectors miss — is in paper/vigil.md.

Build

Standard Android/Gradle. A committed debug keystore signs CI and local builds identically.

./gradlew :app:assembleDebug
# APK -> app/build/outputs/apk/debug/app-debug.apk

CI (.github/workflows/build.yml) builds a debug APK on every push and attaches an APK to a GitHub Release on v* tags.

Status

Prototype. In place: BLE scan + filters, per-ecosystem parsing, Room temporal store, co-movement evaluator with RSSI gate, allowlist, learned baseline, the rotation-clone presence engine (v1, unit-tested), foreground service, Compose UI. Not yet: field-tuning the clone engine on real captures, GATT play-sound / DULT get-identifier, and empirical validation. Before trusting the parser, capture real devices with nRF Connect — the SmartTag2 offsets are inferred from gen-1, and the Tile/AirTag reversing is a couple of years old.

Credits & prior art

Stands on the shoulders of AirGuard (TU Darmstadt / Seemoo-lab) and the SEEMOO Find My research, the IETF DULT working group, Adam Catley's AirTag teardown, and Positive Security's Find You clone research. VIGIL's aim is to go beyond AirGuard on the attacks it structurally cannot catch — see the design doc.

S
Description
No description provided
Readme 1.1 MiB
Languages
Kotlin 100%