|
| 1 | +# Remote DroidGuard for Play Integrity |
| 2 | + |
| 3 | +This document describes how to use remote DroidGuard to obtain Play Integrity attestations from a separate "server" device (typically a stock or properly configured device that passes integrity checks). This allows client devices (e.g. custom ROMs without root or passing attestations) to obtain valid Play Integrity tokens without running DroidGuard locally on the client. |
| 4 | + |
| 5 | +## Background and Motivation |
| 6 | + |
| 7 | +Play Integrity API (and previously SafetyNet) uses DroidGuard for device attestation. microG supports two DroidGuard modes: |
| 8 | + |
| 9 | +- **Embedded** (local): Runs DroidGuard VM locally (default). Requires device to pass attestation. |
| 10 | +- **Network** (remote): Forwards DroidGuard requests over HTTP to a remote server that performs the actual attestation. |
| 11 | + |
| 12 | +Remote mode was historically limited and did not fully support Play Integrity flows because Play Integrity uses a **multi-step** DroidGuard process (init + multiple snapshots with session state, different flows like `pia_attest_e1` and `pia_express`), whereas most other flows are single-step. |
| 13 | + |
| 14 | +This implementation now supports multi-step remote DroidGuard sessions. |
| 15 | + |
| 16 | +## Benefits |
| 17 | + |
| 18 | +- No need to root the daily driver or run obfuscated Google code on it. |
| 19 | +- Offload attestation to a dedicated device at home (or cloud service). |
| 20 | +- Enables commercial "integrity as a service" offerings. |
| 21 | +- Avoids constant maintenance of bypass modules on the client device. |
| 22 | + |
| 23 | +## How it Works |
| 24 | + |
| 25 | +1. On the **client** (your daily device): |
| 26 | + - Enable "DroidGuard" in microG settings → Mode = **Network** |
| 27 | + - Set **Network server URL** to the HTTP endpoint of your remote DroidGuard server. |
| 28 | +2. The client uses `RemoteHandleImpl` which speaks a simple HTTP protocol to the server. |
| 29 | +3. On the **server device** (stock phone or server that passes Play Integrity): |
| 30 | + - Run a remote DroidGuard server app/service. |
| 31 | + - It receives flows + data, calls local `DroidGuard.getClient(...).getResults(...)` (or handle APIs) and returns the result. |
| 32 | +4. For multi-step flows (Play Integrity), the server/client maintain a `sid` (session id) across calls. |
| 33 | + |
| 34 | +## Setting up the Client (microG device) |
| 35 | + |
| 36 | +1. Install/update microG GmsCore that includes the remote DroidGuard fixes (this repo after merge). |
| 37 | +2. Open **microG Settings** → **DroidGuard** (or Device Attestation / Advanced). |
| 38 | +3. Set **Mode** to **Network** (or "Remote"). |
| 39 | +4. Enter the **Network server URL**, e.g.: |
| 40 | + - `http://192.168.1.42:8080/droidguard` |
| 41 | + - `https://my-integrity.example.com/dg` |
| 42 | +5. Optionally enable "Force local disabled" if you want to ensure remote is always used. |
| 43 | +6. Test using Play Integrity API apps (e.g. Play Integrity API Checker) or apps that use Play Integrity (banking, Dott, etc.). |
| 44 | + |
| 45 | +Settings are stored under `SettingsContract.DroidGuard.*`. |
| 46 | + |
| 47 | +## Writing / Running a Remote DroidGuard Server |
| 48 | + |
| 49 | +The remote server must implement a simple HTTP protocol (GET/POST query params + form body). |
| 50 | + |
| 51 | +### Protocol (current implementation) |
| 52 | + |
| 53 | +**URL**: `http://server:port/?flow=XXX&source=com.example&sid=...&x-request-...=...` |
| 54 | + |
| 55 | +- Query params: |
| 56 | + - `flow`: the DroidGuard flow name (e.g. `pia_attest_e1`, `pia_express`, `attest`, `checkin`, `devicekey`, ...) |
| 57 | + - `source`: calling package name |
| 58 | + - `sid`: optional session identifier (for multi-step) |
| 59 | + - `action`: `init` | `close` | omitted (for snapshot) |
| 60 | + - `x-request-*`: values from `DroidGuardResultsRequest.bundle` (may be base64 for bytes) |
| 61 | + |
| 62 | +- Body (POST, urlencoded): key=value pairs from the `snapshot(map)` data. |
| 63 | + |
| 64 | +**Response**: |
| 65 | +- For single step or snapshot: base64 (URL-safe, no padding) of the DroidGuard result. |
| 66 | +- For init that returns a new session: `SID|base64result` (pipe separated) or just the SID. |
| 67 | +- Errors: start with `ERROR ` or HTTP error status. |
| 68 | + |
| 69 | +The server should: |
| 70 | + |
| 71 | +1. On `action=init` or first call: create a handle / call `DroidGuard.getClient(ctx).getResults(flow, data, request)` or use the full handle `initWithRequest` + `snapshot`. |
| 72 | +2. For Play Integrity multi-step (pia_attest_e1 / pia_express), keep state keyed by `sid`. |
| 73 | +3. Return the raw result bytes (as returned by DroidGuard) base64 encoded. |
| 74 | + |
| 75 | +### Minimal Reference Server (Kotlin + Ktor or plain HttpServer) |
| 76 | + |
| 77 | +A simple reference implementation can be written as a standalone JVM app or Android service. |
| 78 | + |
| 79 | +Here is a minimal example using Java's built-in `com.sun.net.httpserver.HttpServer` (no external deps) that runs on any Android device with microG/GMS that has DroidGuard available locally: |
| 80 | + |
| 81 | +```kotlin |
| 82 | +// Example: RemoteDroidGuardServer.kt |
| 83 | +// Compile/run as part of a minimal Android app or use on desktop with a fake context if possible. |
| 84 | +// For phone, package as an app with a foreground service exposing the port. |
| 85 | + |
| 86 | +import android.content.Context |
| 87 | +import com.google.android.gms.droidguard.DroidGuard |
| 88 | +import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest |
| 89 | +import com.google.android.gms.tasks.Tasks |
| 90 | +import fi.iki.elonen.NanoHTTPD // or use built-in server; example simplified |
| 91 | +import java.net.InetSocketAddress |
| 92 | +import java.util.* |
| 93 | +import java.util.concurrent.ConcurrentHashMap |
| 94 | +import android.util.Base64 |
| 95 | +import java.net.URLDecoder |
| 96 | +import java.net.URLEncoder |
| 97 | + |
| 98 | +class RemoteDroidGuardServer(private val context: Context, port: Int = 8080) : NanoHTTPD(port) { |
| 99 | + private val sessions = ConcurrentHashMap<String, Any>() // placeholder for state |
| 100 | + |
| 101 | + override fun serve(session: IHTTPSession): Response { |
| 102 | + val params = session.parms |
| 103 | + val flow = params["flow"] ?: return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "ERROR no flow") |
| 104 | + val source = params["source"] ?: "unknown" |
| 105 | + val sid = params["sid"] |
| 106 | + val action = params["action"] |
| 107 | + |
| 108 | + val request = DroidGuardResultsRequest() |
| 109 | + params.filterKeys { it.startsWith("x-request-") }.forEach { (k, v) -> |
| 110 | + val realKey = k.removePrefix("x-request-") |
| 111 | + // handle base64 etc. |
| 112 | + try { |
| 113 | + request.bundle.putString(realKey, URLDecoder.decode(v, "UTF-8")) |
| 114 | + } catch (_: Exception) {} |
| 115 | + } |
| 116 | + |
| 117 | + val data = mutableMapOf<String, String>() |
| 118 | + if (session.method == Method.POST) { |
| 119 | + val body = session.inputStream.bufferedReader().readText() |
| 120 | + body.split("&").forEach { |
| 121 | + val (k,v) = it.split("=", limit=2).let { p -> p[0] to URLDecoder.decode(p.getOrNull(1)?:"", "UTF-8") } |
| 122 | + data[k] = v |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + return try { |
| 127 | + val result = if (action == "init") { |
| 128 | + // For multi-step, we could create handle and store |
| 129 | + val token = DroidGuard.getClient(context).getResults(flow, data, request).get() // or await |
| 130 | + // Generate sid if needed for pia flows |
| 131 | + val newSid = UUID.randomUUID().toString() |
| 132 | + sessions[newSid] = Unit // store handle if advanced |
| 133 | + "$newSid|${Base64.encodeToString(token.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)}" |
| 134 | + } else if (sid != null && sessions.containsKey(sid) && action == "close") { |
| 135 | + sessions.remove(sid) |
| 136 | + "CLOSED" |
| 137 | + } else { |
| 138 | + // snapshot or simple |
| 139 | + val token = DroidGuard.getClient(context).getResults(flow, data, request).get() |
| 140 | + Base64.encodeToString(token.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) |
| 141 | + } |
| 142 | + newFixedLengthResponse(Response.Status.OK, "text/plain", result) |
| 143 | + } catch (e: Exception) { |
| 144 | + newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "ERROR ${e.message}") |
| 145 | + } |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +**Notes**: |
| 151 | +- For full multi-step support (init/snapshot/close on handle), the server should use `DroidGuardHandle` API instead of the convenience `getResults`. |
| 152 | +- In the client `RemoteHandleImpl`, we added `sid` and `action` support. |
| 153 | +- To run on phone: create a small APK with this server (use NanoHTTPD or OkHttp server or Ktor). Expose via a foreground service + notification. Use `adb forward` or open port on WiFi. |
| 154 | +- For production: add auth (API key), TLS (self-signed or Let's Encrypt), rate limiting. |
| 155 | + |
| 156 | +### Alternative: Use the archived microg/RemoteDroidGuard as starting point |
| 157 | + |
| 158 | +The original remote implementation lived at https://github.com/microg/RemoteDroidGuard (archived). You can adapt its service to expose the HTTP endpoint described above. |
| 159 | + |
| 160 | +## Running the Server on a Stock / Passing Device |
| 161 | + |
| 162 | +### Recommended Setup for Server Device |
| 163 | + |
| 164 | +- Use a stock ROM or a device that can pass **DEVICE** (or **STRONG**) integrity with real Google Play Services. |
| 165 | +- Or use LineageOS + microG + PlayIntegrityFix + TrickyStore + valid keybox (as described in community guides). |
| 166 | +- Keep the server device powered on and connected to the same network (or exposed via reverse proxy / Tailscale / ngrok for remote access). |
| 167 | +- Run the Remote DroidGuard server app persistently (foreground service). |
| 168 | +- Optionally run it headless on a Raspberry Pi with Android or use an old phone. |
| 169 | + |
| 170 | +### Exposing Securely |
| 171 | + |
| 172 | +- Local WiFi only (recommended for privacy). |
| 173 | +- Use Tailscale / ZeroTier / WireGuard. |
| 174 | +- Cloud: run a small VPS with Android-x86 or use a commercial service (future). |
| 175 | + |
| 176 | +## Testing |
| 177 | + |
| 178 | +1. On client set remote URL. |
| 179 | +2. Use an app like: |
| 180 | + - [Play Integrity API Checker](https://play.google.com/store/apps/details?id=com.google.android.play.core.integrity.verifier) (or forks) |
| 181 | + - Banking / ride apps that use Play Integrity. |
| 182 | +3. Check logs in microG / logcat for "RemoteGuardImpl". |
| 183 | +4. On server side: watch for incoming requests. |
| 184 | + |
| 185 | +## Limitations & Future Work |
| 186 | + |
| 187 | +- Currently the remote implementation returns `null` for `initWithReply` (PFD objects). Full low-latency reply support may require more work. |
| 188 | +- Session state on server is currently in-memory; restart loses sessions (fine for most flows). |
| 189 | +- Hardware-backed strong integrity may require the server device to have a valid keybox / TEE state. |
| 190 | +- Authentication / multi-tenancy for public servers not implemented. |
| 191 | + |
| 192 | +## Related Code |
| 193 | + |
| 194 | +- `play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/RemoteHandleImpl.kt` |
| 195 | +- `DroidGuardServiceImpl.kt` |
| 196 | +- `DroidGuardClientImpl.java` |
| 197 | +- `IntegrityService.kt` and `IntegrityExtensions.kt` (for pia_attest_e1 / pia_express flows) |
| 198 | +- `PoTokenHelper.kt` |
| 199 | + |
| 200 | +## Contributing |
| 201 | + |
| 202 | +Improvements to multi-step handling, a reference server app, and docs are welcome. |
| 203 | + |
| 204 | +## References |
| 205 | + |
| 206 | +- Original issue: https://github.com/microg/GmsCore/issues/2851 |
| 207 | +- RemoteDroidGuard (historical): https://github.com/microg/RemoteDroidGuard |
| 208 | +- DroidGuard deep dive papers and community guides for passing integrity on custom devices. |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +*This feature enables privacy-friendly and maintainable Play Integrity attestation.* |
0 commit comments