Repository navigation
feat: send device fingerprint so machine binding works on Android - #33
Merged
Merged
Conversation
added 2 commits
October 6, 2026 08:58
- MachineFingerprint (core:data): SHA-256 of ANDROID_ID as 64 lowercase hex, unchanged from the value the app already sent so tokens that are bound keep working. Missing, empty or broken ANDROID_ID falls back to a random 32-byte ID kept in EncryptedStorage (survives a setup reset) instead of the shared hash of "unknown". Cached in memory, only the first 8 chars are ever logged. - MachineFingerprintInterceptor: network interceptor per server client, so X-Machine-Fingerprint goes only to the configured GateControl server and is stripped on redirects to other hosts. - Enroll (setup code redeem) sends `fingerprint` in the body as well. - MachineBindingError / MachineBindingMonitor: binding mismatch, fingerprint_required and invalid fingerprint are recognised (en/de server messages and enroll codes) and shown as clear messages in setup, the support bundle result and a banner on the VPN screen. - Settings shows the device ID short form (first 8 hex chars) to match the server's "Gebunden an Gerät ab12cd34…"; the support bundle includes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
OkHttp strips only Authorization on cross-host redirects, not custom headers, so a redirect from the server (or a MITM on plain HTTP) would hand X-API-Token to a foreign host. The per-server network interceptor (MachineFingerprintInterceptor, now ServerScopedHeadersInterceptor) runs on every hop and removes the token, including an explicit connection-test token, whenever scheme, host or port differ from the configured server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Machine binding (Gerätebindung) on the GateControl server now works for the Android client end to end: the fingerprint is stable and unique per device, it reaches only the configured server, it is sent when a setup code is redeemed, and binding rejections show a clear message.
Fingerprint (
core/data/.../MachineFingerprint.kt)SHA-256(ANDROID_ID), 64 lowercase hex (^[a-f0-9]{64}$). Since Android 8, ANDROID_ID is already scoped per signing key, user and device. It survives reinstalls and changes on a factory reset.X-Machine-Fingerprintwas introduced (oldMachineId). Adding a package name or salt would change the value, and every Android token that is already bound would then fail with "bound to a different machine". So the derivation is kept on purpose."unknown", so all such devices shared one fingerprint. Now a null, empty, all-zero or9774d56d682e549cANDROID_ID falls back to a random 32-byte ID. It is generated once and stored in the app's Keystore-backedEncryptedStorage, and it is kept when the setup is reset (EncryptedStorage.clear(keep = …)).Requests
ServerScopedHeadersInterceptor(originallyMachineFingerprintInterceptor) is a network interceptor on each server's OkHttp client. It runs once per hop, so the header goes only to the configured GateControl server (same scheme, host and port). It is removed if a redirect goes to another host. This covers everyApiClientcall: ping, register, enroll, config/check, heartbeat, peer-info, traffic, policy, RDP, support bundle, update check, services, split-tunnel, Pi-hole.AuthInterceptorno longer adds the fingerprint. The header stays redacted in the HTTP log.EnrollRequestnow also carriesfingerprintin the body. The current server reads the header on/client/enroll, and the body field is there for the contract.API token stays on the server (separate commit)
Authorizationon cross-host redirects, not custom headers. A redirect from the server, or a man-in-the-middle on plain HTTP, would therefore passX-API-Tokento a foreign host.X-API-Tokenwhenever the scheme, host or port differ from the configured server. This includes the explicit token of a connection test.Error handling
MachineBindingError(MISMATCH, REQUIRED, INVALID) recognises the server's messages in English and German and the enroll codefingerprint_required.MachineBindingMonitor(an application interceptor) records binding rejections from any client API call, including background ones. A banner on the VPN screen shows them. The next successful call to an endpoint that checks the binding clears the banner, for example after an admin resets the binding.values/andvalues-de/):binding_mismatch: "Dieser Zugang ist an ein anderes Gerät gebunden. Bitte den Administrator, die Gerätebindung zurückzusetzen." / "This access is bound to a different device. Ask your administrator to reset the device binding."binding_required,binding_invalid,settings_device_idDevice ID display
Geräte-ID ab12cd34…/Device ID ab12cd34…. This matches the server's "Gebunden an Gerät ab12cd34…".deviceId(short form only).Tests
MachineFingerprintTest: format, sha256 compatibility, stability and caching, the fallback for each invalid ANDROID_ID, fallback reuse, no shared fallback, a corrupt stored fallback, and an exception while reading ANDROID_ID.MachineFingerprintRequestTest(MockWebServer, realApiClientProviderwiring):X-API-Tokenis present on same-host calls and absent after a redirect to a foreign host, including an explicit test token and a host that differs only in port.EncryptedStorageTest:clear(keep).SetupViewModelTest: the enroll body has the fingerprint, and enrollfingerprint_requiredand register 403 mismatch map to the new messages.SettingsViewModelTest: the device ID is shown, the support bundle 403 mismatch maps to the binding message, and the bundle containsdeviceId../gradlew testand./gradlew lintReleasepass locally.🤖 Generated with Claude Code
https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
Generated by Claude Code