0fa6a2dd2a
paper/vigil.md: per-ecosystem detection methodology (Apple/FMDN/Samsung/Tile/DULT) and the rotation-clone presence engine (identity-agnostic detection, the rotation-rate squeeze, FP analysis, honest floor). Draft working paper accompanying the reference implementation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
144 lines
6.9 KiB
Markdown
144 lines
6.9 KiB
Markdown
# 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)
|
||
```
|
||
|
||
## 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](paper/vigil.md)**.
|
||
|
||
## 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.
|