Skip to content

Commit e4f04e5

Browse files
authored
Merge pull request #1 from Karan02204/arena/019fe4c4-gmscore
droidguard: Support multi-step remote DroidGuard for Play Integrity
2 parents 9a206ae + 49c1fa5 commit e4f04e5

3 files changed

Lines changed: 364 additions & 7 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Simple reference Remote DroidGuard server (for illustration).
3+
* Package this into an Android app + foreground service.
4+
* Requires: implementation of a HTTP server (NanoHTTPD, Ktor, etc.)
5+
* On device that has working local DroidGuard (stock or properly modded phone).
6+
*/
7+
8+
import android.content.Context
9+
import android.util.Base64
10+
import com.google.android.gms.droidguard.DroidGuard
11+
import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest
12+
import java.net.URLDecoder
13+
import java.util.concurrent.ConcurrentHashMap
14+
15+
// Pseudocode - adapt to your HTTP server library
16+
class RemoteDroidGuardHttpHandler(private val androidContext: Context) {
17+
18+
private val activeSessions = ConcurrentHashMap<String, String>() // sid -> state
19+
20+
fun handleRequest(query: Map<String, String>, postBody: String?): String {
21+
val flow = query["flow"] ?: return "ERROR: no flow"
22+
val sourcePkg = query["source"] ?: "unknown"
23+
val sid = query["sid"]
24+
val action = query["action"]
25+
26+
val dgRequest = DroidGuardResultsRequest().apply {
27+
query.filterKeys { it.startsWith("x-request-") }.forEach { (k, v) ->
28+
val key = k.removePrefix("x-request-")
29+
// Decode if it looks base64
30+
val decoded = try { Base64.decode(v, Base64.NO_WRAP) } catch (_: Exception) { null }
31+
if (decoded != null && decoded.isNotEmpty()) {
32+
bundle.putByteArray(key, decoded)
33+
} else {
34+
bundle.putString(key, URLDecoder.decode(v, "UTF-8"))
35+
}
36+
}
37+
}
38+
39+
val dataMap = mutableMapOf<String, String>()
40+
postBody?.split("&")?.forEach { pair ->
41+
val parts = pair.split("=", limit = 2)
42+
if (parts.size == 2) {
43+
dataMap[URLDecoder.decode(parts[0], "UTF-8")] =
44+
URLDecoder.decode(parts[1], "UTF-8")
45+
}
46+
}
47+
48+
return try {
49+
when {
50+
action == "init" -> {
51+
// Start multi-step session for Play Integrity
52+
val newSid = java.util.UUID.randomUUID().toString()
53+
// Optional: actually create DroidGuardHandle here and store it
54+
activeSessions[newSid] = flow
55+
// Perform an initial getResults or init
56+
val initial = DroidGuard.getClient(androidContext)
57+
.getResults(flow, dataMap, dgRequest).get()
58+
"$newSid|${Base64.encodeToString(initial.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)}"
59+
}
60+
action == "close" && sid != null -> {
61+
activeSessions.remove(sid)
62+
"CLOSED"
63+
}
64+
else -> {
65+
// Regular snapshot or single step
66+
val result = DroidGuard.getClient(androidContext)
67+
.getResults(flow, dataMap, dgRequest).get()
68+
Base64.encodeToString(result.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
69+
}
70+
}
71+
} catch (e: Exception) {
72+
"ERROR: ${e.javaClass.simpleName}: ${e.message}"
73+
}
74+
}
75+
}

docs/remote-droidguard.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)