VIGIL 0.1.0 — temporal counter-tracking (prototype)

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
This commit is contained in:
Kara Zajac
2026-07-15 00:11:00 -04:00
commit 817a4ac1c9
40 changed files with 2585 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
# 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](https://github.com/KaraZajac/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](#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](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.
```bash
./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](https://github.com/seemoo-lab/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.