TacMap Threat Model

Status: maintained release document · Audience: users, unit security staff, code auditors Scope: what information TacMap can expose, to whom, and where its guarantees stop.

This document is deliberately plain. If you use TacMap to hold or share unit information, you should be able to read this in five minutes and know exactly what leaves your device, what a hostile party could learn, and what the app does not protect you from.

Nothing here is marketing. Where a guarantee has a limit, the limit is stated.


1. What TacMap is

An offline-first tactical mapping tool for iOS and Android: MGRS grid, drawing and measurement, military symbology, waypoints and track logs, import/export, and an optional unit sync feature that shares live presence, map objects, and live TacMap Chat messages between devices that share a join code.

Core navigation, imported-map, drawing, measurement, and mission-object work can run without a network. Unit Sync requires a path to its relay. Online tiles and lookups require their separately enabled providers, and platform-store ownership reconciliation can occur independently of those OPSEC gates.


2. Critical information (what an adversary wants)

If TacMap is holding real data, the sensitive items are:

The last two matter even when the first three are encrypted. Keep reading.


3. Trust boundaries

TacMap treats the following as untrusted once data crosses into them:

BoundaryTrusted?Why it matters
Your deviceTrusted (see §7 caveats)Holds the at-rest key, and can decrypt mission data.
The sync relayUntrustedRoutes encrypted traffic; can see metadata.
Basemap / lookup providersUntrustedSee the coordinates you request.
The network path (ISP, Wi-Fi, carrier)UntrustedSees who you talk to and when.
Other members of your sync roomPartly trusted by youThey hold the room key and see room-shared content. A Selected unit chat uses a separate pairwise session key, but its recipient can still copy the plaintext.

The design goal is that only your device and the intended sharing scope see mission payloads in cleartext. Room objects, presence, and Entire room chat are readable by room-key holders. Selected unit chat is readable only by the two selected live endpoint sessions. The relay sees ciphertext together with the clear protocol metadata and control fields documented below. Tile/lookup providers see the coordinates or queries required for an action you enable.


4. What the sync relay can and cannot see

This is the core statement. Read it before trusting sync.

One-paragraph version. When you join a sync room, your device turns the human join code (e.g. bravo-tonight) into three separate values using a slow password-stretch (PBKDF2-HMAC-SHA256, 210,000 iterations). One value is a routing ID the relay uses to connect you to your room. A second is the encryption key, which never leaves your device. A third is an authorization token sent to the relay inside the TLS-protected WebSocket handshake; the relay hashes and pins it for room admission. Every map object and every presence update is sealed with AES-256-GCM using the encryption key before it is sent. The relay forwards and may retain that ciphertext, but it also handles clear routing, object/version/kind, request/acknowledgement, actor, session, chat scope and recipient routing, and control fields. It cannot reverse the routing ID straight back to the key. But the routing ID and the key are both derived from the same join code, so the protection is only ever as strong as that code: a weak or human-memorable code can be guessed offline (see Your join code is the whole ballgame, below). With a strong, generated code the relay can route the traffic and inspect the metadata listed below, but—absent compromise of a client or the code—it cannot decrypt correctly authenticated mission-payload ciphertext.

What the relay CAN see:

What the relay CANNOT see:

What payload authentication prevents. Each sealed object binds its own routing metadata (object ID, version, kind) into the encryption as associated data. A relay that relabels an object under a different ID or moves it between rooms produces an authentication failure and the client rejects it. An outside party with only a leaked routing ID also cannot pass room admission without the separate authorization token. The relay can still suppress, delay, replay, or serve older genuinely authenticated state; those limits are explicit in §7.

TacMap Chat applies the same rule to recipient scope. The binary header bound into both AES-GCM and the sender's Ed25519 signature contains the room, sender, session, counter, message ID, key ID, scope, and—when selected—the exact recipient actor/session/key. A relay cannot turn a selected-unit message into an Entire room message or silently retarget it. Selected-unit encryption uses a session X25519 secret that neither the relay nor other join-code holders have.

The honest limit. Content is protected. Metadata is not. A relay operator, or anyone who compromises or coerces the relay or its host, can learn that a group of IPs form a unit, roughly where they are, and when they are active. Selected-unit routing also exposes who is messaging whom, even though the words remain encrypted. That is enough to infer association and operational tempo. See §7.

The member list is also not independent proof of liveness. A signed session announcement authenticates the device that created it, but the relay decides when to forward a join or leave and can replay, delay, suppress, or reorder genuinely signed session state. The UI therefore describes members as relay-reported sessions; confirm critical presence out-of-band.

Your join code is the whole ballgame. The routing ID, the encryption key, and the write token are all derived from the join code through one fixed, app-wide salt. There is no per-room salt on purpose: the relay has to route by a value anyone with the code can compute. Two things follow. Because the salt is a constant, an attacker can precompute a code -> (routing ID, key, token) dictionary once and reuse it against every room, forever. And because the relay sees every routing ID, that ID is a free offline confirmation oracle: guess codes, derive their routing IDs, match them against observed rooms, and any hit hands over the encryption key (read) and the write token (write). PBKDF2 at 210,000 iterations makes each guess cost real work, but a short or memorable code - a couple of dictionary words like bravo-tonight - still falls to a GPU rig in minutes. So sync is only private if your join code has real entropy. The app generates a strong ~78-bit code for you and refuses codes under 14 characters; use the generator and pass the code out-of-band. A code you invent yourself, especially a memorable one, is guessable and is not covered by the guarantees above. (A memory-hard KDF - Argon2id/scrypt - would raise the per-guess cost further and is a planned hardening; it does not remove the need for an entropic code.)

The relay is auditable: its payloads are sealed, while its source shows the clear admission token, envelope metadata, actor/session records, and control traffic it also handles. You do not have to trust ours. You can self-host it (see §8).

Crypto reference for auditors: SyncCrypto.kt / SyncCrypto.swift, the TacMap Chat crypto implementations, ADR-004, and their test suites. Android and iOS produce byte-identical sealed format (iv(12) || ciphertext || tag(16)).


5. Network egress table

Every outbound connection TacMap can make, what triggers it, what the far end learns, and its default state. The online-map and lookup gates start off for a fresh install and can be enabled independently. An update preserves choices that an existing user already stored.

EndpointPurposeTriggered byWhat the provider learnsDefaultMitigation
ibasemaps-api.arcgis.com (Esri World Imagery)Satellite basemap tiles (the initial style after opt-in)Viewing the map with online basemaps enabledYour IP + the tile coordinates/zoom you view = your area of interest, over timeOff until online basemaps are enabledLeave the gate off; use offline packs, see §6
static-map-tiles-api.arcgis.com (Esri)Topographic + OSM-street basemap tiles (licensed)Selecting those styles with online basemaps enabledYour IP + requested tile coordinates = your AOOff until online basemaps are enabledLeave the gate off; use offline packs
a.tile.opentopomap.org, b.tile.opentopomap.org, c.tile.opentopomap.orgOpenTopoMap community topo tiles (the one keyless style)Selecting the OSM-Topo style with online basemaps enabledYour IP + requested tile coordinates = your AOOff until online basemaps are enabledLeave the gate off; use offline packs
api.open-meteo.com/v1/forecastWeather lookupOpening the weather dialog after enabling online lookupsYour IP + the map-centre coordinate, coarsened to roughly 110 mOff until online lookups are enabledLeave online lookups off
api.open-meteo.com/v1/elevationElevation + terrain heatmapElevation/terrain features after enabling online lookupsYour IP + the map-centre coordinate coarsened to roughly 110 m for elevation, or a 24 × 24 coordinate grid coarsened to roughly 11 m covering the visible area for terrainOff until online lookups are enabledLeave online lookups off
Place-name search — iOS MKLocalSearch (Apple), Android platform GeocoderTurning a typed place name into a coordinateTyping 2+ non-coordinate characters in Search after online lookups are enabledYour IP + the search string; iOS also supplies the camera region and Android can supply a location bias. These can reveal your AO (for example, searching a FOB/village name)Off until online lookups are enabled; coordinate-shaped text stays localLeave online lookups off; navigate by MGRS/grid instead
Sync relay (default: tacmap-sync.<...>.workers.dev)Encrypted unit sync and live TacMap Chat transportJoining a sync roomEncrypted mission/chat payloads; routing ID; admission token during the handshake; clear envelope, actor/session/key IDs, chat scope and selected recipient, request/acknowledgement and control fields; your IP and traffic timing/size (see §4)Off until you join a roomSelf-host the relay; see §8
Apple App Store / Google PlayOwnership reconciliation, product details, purchase, restore, redemptionApp launch and throttled foreground refresh; product/price loading while a paywall is visible; user purchase/restore/redeem actionsStandard store account, app/product, transaction/token, device, IP, timing, and diagnostic metadata; no map, location, or Unit Sync payloadLifecycle reconciliation can occur even when OPSEC gates are offAirplane mode or external network policy is the only complete suppression; see ADR-003

The three basemap rows apply to both platforms — iOS and Android draw the same Esri/OSM raster tiles only after explicit opt-in. Neither app uses Apple Maps or Google Maps for basemap rendering (§6). On iOS, the custom renderer does not create an MKMapView, so Apple's geod daemon is not asked for basemap tiles (measured zero, §6). On Android, the Google Maps SDK has been removed from the app entirely (§6). Apple's MKLocalSearch remains an opt-in place-search path and is listed separately above.

Read this table as the whole story. If an endpoint is not listed here, the app does not contact it. We add no analytics SDK, remote crash telemetry, or ad network; our own crash reports are written to local storage only and shared by you manually. Neither platform instantiates a third-party basemap engine. iOS links Apple frameworks for coordinate types and opt-in place search, not basemap rendering. Play Billing and StoreKit may reconcile entitlement at launch/foreground as well as during user purchase actions; see ADR-003.


6. The area-of-interest problem (basemaps and lookups)

Even with sync fully off, requesting online map tiles or online weather/elevation tells the provider which ground you care about. Panning to a grid square fetches tiles for that square from your IP. A provider, or anyone with access to its logs, can reconstruct your area of interest and how it moves over time. This is the same class of exposure as the 2018 fitness-app heatmap incident.

TacMap's controls:

The two platforms, both now closed

Both platforms draw the same Esri/OSM raster tiles only after explicit opt-in, and render through app-owned tile engines rather than an Apple or Google basemap view. The map is a Compose/Canvas renderer on Android and a custom UIKit renderer on iOS. Each draws only the raster source you chose: an Esri/OSM online style when the basemap gate is on, an offline pack/GeoPDF when you have imported one, or nothing when both are off.

That is a change from earlier builds, where Android hosted the map in Google's SDK. With online basemaps off, that SDK still fetched no basemap tiles — but the SDK itself phoned home on launch (~280 KB cold / ~24 KB warm on a Pixel emulator) for provisioning and telemetry, whatever the tile gate said. That check-in carried your IP and the fact that a Google-Maps app had started (not the coordinates you viewed — that's what the tile gate stops). That is now gone. The Google Maps SDK has been removed from the app: the three Maps dependencies are dropped, the com.google.android.geo.API_KEY manifest entry and MapsInitializer call are deleted, and the built APK's dex contains zero com.google.android.gms.maps or com.google.maps.android classes (verifiable with dexdump on any release build). There is no longer any code path that performs the provisioning check-in, because the code that performed it is not in the binary.

So on Android, as on iOS, with the basemap gate off the app makes no basemap tile request; with it on, tiles go to Esri/OpenTopoMap (your choice, behind the persistent red banner). On a device with Google Play Services, system GMS processes can have ambient network traffic of their own. Separately, TacMap's Play Billing integration can bind to the Play Store for the entitlement contacts listed in §5; those requests do not include map or unit payloads. When no network contact at all is acceptable, disable all relevant radios or use an externally enforced network policy and verify the actual device configuration.

The iOS side, now closed

For most of this app's life, iOS had a hole here we could not close from inside MapKit. MapKit has no "no basemap" mode; the only way to suppress Apple's basemap was to cover it with a canReplaceMapContent overlay, which stops MapKit drawing the basemap but not fetching it. Apple's tiles are pulled by geod, a system daemon outside our sandbox, which kept fetching tiles for the on-screen region no matter what we drew on top. Measured on a freshly erased iPhone 17 Pro simulator, sitting on the map for 35 seconds grew geod's tile store (Caches/com.apple.geod/Vault/MapTiles) by ~457 KB whether the basemap toggle was on or off — the same tiles either way.

As of build 33 this is fixed. iOS no longer uses MKMapView at all. The map is rendered by an in-app tile renderer we wrote (TileMapView), which draws only the raster source you chose — an Esri/OSM online style when the basemap gate is on, an offline pack/GeoPDF when you've imported one, or nothing when both are off. There is no MKMapView in the tree, so geod is never asked for a tile.

This is measured, the same WAL way. After checkpointing geod's MapTiles.sqlitedb-wal to zero and then panning the renderer aggressively across fresh ground:

Map enginegeod tile-store growth while panning
Old MKMapView~457 KB (fetched regardless of the gate)
New in-app renderer0 bytes

During that pan the app fetched tiles the whole time — every request went to ibasemaps-api.arcgis.com (the Esri basemap you turned on), with zero basemap-tile contact to any Apple map host (*.ls.apple.com, gspe*, cdn.apple-mapkit). So on iOS the AO no longer leaks to Apple through the basemap. (One thing still can: place-name search uses MKLocalSearch, which sends the query you type and camera region to Apple's servers - but only if you enable online lookups and use the search box. See §5.) What the online-basemaps gate now buys you is the ordinary thing it says: with it off, the app makes no basemap tile request of any kind, and an imported offline pack or GeoPDF genuinely hides your AO — there is no Apple fetch underneath it any more.

The remaining ambient exposure is now symmetric across both platforms and comes only from the providers you can still choose to use: turning the basemap gate on sends your tile coordinates to Esri/OpenTopoMap (your choice, with a persistent red banner while it is active). No Apple or Google basemap engine is started on either platform.

Rule of thumb: an online map or coordinate lookup exposes your AO to its provider. Pre-stage offline maps before you need them. With both online gates off, the in-app renderers make no basemap or lookup request on either platform; Unit Sync, store reconciliation, operating-system traffic, and any other app on the device remain separate network paths.


7. What TacMap does NOT protect you from

Stated plainly, because a tool that hides its limits cannot be trusted.


Fresh-install defaults:

For real operations, additionally:


9. For auditors

Issues and disclosures welcome via the repository.


This is maintained release documentation. Revalidate it against every candidate build; planned work and unverified device-specific behaviour are not guarantees.