Files
VIGIL/README.md
T
Kara Zajac f4635967c7
Build APK / build (push) Waiting to run
Use Radar Eye logo in the app header + README
- App: top-bar brand mark is now the Radar Eye (drawable/vigil_logo.xml) instead of a generic shield.
- README: centered logo header (docs/logo.png, rendered from docs/logo.svg).
Release 0.1.7.

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

8.0 KiB
Raw Blame History

VIGIL

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. Detection is entirely passive — it listens only. The one exception is the user-initiated "Make it ring" action, which connects to a tracker you already suspect and asks it to play a sound (the DULT-standard way for a victim to locate a hidden tag); nothing is transmitted unless you tap it.


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.

Finding a tracker

Tap any tracker for two ways to physically locate it:

  • Make it ring — connects over GATT and plays the tracker's own sound. Works for AirTags (native 0xAF sound), Google Find My Device, and DULT tags (Chipolo, Pebblebee, eufy, Motorola). Samsung SmartTag and Tile expose no non-owner ring, so VIGIL points you to the SmartThings / Tile app instead.
  • Hot/cold finder — a passive proximity meter that turns live signal strength into a warmer/colder readout. It still works on silent or modified tags that refuse to ring, which is exactly when you need it most.

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.