Android app that detects personal item-trackers (AirTag, Tile, Samsung SmartTag, Google Find My Device / DULT) following the user over time. Sibling to OVERWATCH: OVERWATCH is spatial, VIGIL is temporal. - BLE scan + per-ecosystem wire-format parsing (Apple 0x004C/0x12, FMDN 0xFEAA, Samsung 0xFD5A, Tile 0xFEED/FEEC, DULT 0xFCB2) - Room temporal store (14-day sightings), co-movement evaluator with RSSI proximity gate - Allowlist + learned offline baseline (auto-trust household tags) - Foreground service, Compose UI (Catppuccin Mocha) - Privacy: NO INTERNET permission — fully on-device - Reuses OVERWATCH Gradle setup + committed debug keystore - docs/detection-rotation-clone.md: design for the rotation-clone presence engine (#1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
6.6 KiB
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 spatial — what surveillance is watching this place, right now. VIGIL is temporal — what 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.
The detect/ package ships the co-movement v1; the presence engine slots in next.
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)
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, foreground service, Compose UI. Not yet: the rotation-clone presence engine (designed, not wired), 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.