Registration
------------
On first upload the app trades the bootstrap token and its device id at
/api/v1/register-device for a key belonging to this device alone, keeps it in
the Keystore-encrypted store, and uses it from then on. The bootstrap token is
never sent again.
The only secret shipped in the APK now grants enrolment and nothing else.
Extracting it lets someone register a device; it does not let them read points
or upload as a phone that is already enrolled.
A 401 on upload means the key was revoked or rotated elsewhere. The uploader
forgets it, registers again and retries once, rather than leaving the user to
find the Server dialog.
The registration URL is derived from the configured upload URL, so pointing
the app at another server moves both together.
Keystore storage
----------------
The AES-GCM/Keystore code moves out of DeviceIdentity into SecureStore, now
that there are two secrets to keep rather than one. Below API 23 there is no
Keystore AES: SecureStore refuses to write instead of silently storing
secrets in the clear, and both callers keep their value in memory for the
process lifetime.
Upload host
-----------
DEFAULT_URL moves to com.org.bz. bt.justbug.me resolves elsewhere and has no
certificate on the proxy, so every upload there died in the TLS handshake with
TLSV1_ALERT_UNRECOGNIZED_NAME before a request was sent. Installs that already
saved the old default are migrated across on read; a URL the user chose is
left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sends X-Device-ID on every upload so the server can attribute sightings to the
phone that recorded them; it now rejects uploads without one.
Prefers Settings.Secure.ANDROID_ID: it survives reinstalls, costs nothing to
read and needs no permission. It is not always trustworthy though - null
before first boot completes, and a known family of builds all report the same
constant - so those readings are rejected outright rather than repaired, and a
random UUID is generated and kept instead. Guessing at a malformed reading
would produce an identifier that changes between reads, which is worse than
falling back.
The fallback UUID is stored encrypted under an AES-GCM key held in the
AndroidKeyStore, so the key material never enters the app process and cannot
be lifted out of a backup or off a rooted device. androidx.security's
EncryptedSharedPreferences does exactly this, but it needs AndroidX and API
23; this project builds in AIDE with no dependency resolution and minSdk 14,
so the same construction is done directly against the platform Keystore. The
Keystore calls sit behind SDK_INT checks and are never resolved below 23,
where the identifier is kept in memory for the process lifetime rather than
written out in the clear.
A stored value that will not decrypt - key cleared, or restored onto another
device - yields a new identity rather than a crash. The old one is genuinely
unrecoverable at that point.
Upload errors now surface the server's "error" field instead of the raw JSON
envelope, so a rejected device ID reads as a sentence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>