Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.microg.gms.droidguard

import android.content.Context
import android.util.Log
import org.microg.gms.settings.SettingsContract

class DroidGuardProvider {
private var localDroidGuard: DroidGuardLocal? = null
private var remoteDroidGuard: DroidGuardRemote? = null
private var context: Context? = null

fun initialize(context: Context) {
this.context = context
val remote = remoteDroidGuard
return if (remote != null && shouldUseRemote()) {
Log.d(TAG, "Using remote DroidGuard")
remote.getResult(context!!, request)
} else {
Log.d(TAG, "Using local DroidGuard")
localDroidGuard?.getResult(request) ?: throw IllegalStateException("No DroidGuard implementation available")
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.microg.gms.droidguard

import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.droidguard.DroidGuardRequest
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.net.Socket
import java.nio.ByteBuffer


private const val DG_PROTOCOL_VERSION = 1
private const val DG_CMD_GET_RESULT = 1
private const val DG_CMD_GET_RESULT_MULTI = 2
private const val DG_CMD_GET_RESULT_MULTI_CONTINUE = 3

data class RemoteDroidGuardConfig(
val host: String,
)

class DroidGuardRemote(private val config: RemoteDroidGuardConfig) {
suspend fun getResult(context: Context, request: DroidGuardRequest): DroidGuardResult = withContext(Dispatchers.IO) {
Socket(config.host, config.port).use { socket ->
val output = DataOutputStream(socket.getOutputStream())
val input = DataInputStream(socket.getInputStream())
// Write protocol version
output.writeInt(DG_PROTOCOL_VERSION)

// Check if this is a multi-step request
val isMultiStep = request.javaClass.getDeclaredFields().any { it.name == "flow" || it.name == "session" }

if (isMultiStep) {
// Use multi-step protocol
output.writeInt(DG_CMD_GET_RESULT_MULTI)
writeRequest(output, request)
output.flush()

// Handle multi-step flow
var step = 0
while (true) {
val responseType = input.readInt()
when (responseType) {
1 -> { // Need more data
val extraDataLength = input.readInt()
val extraData = ByteArray(extraDataLength)
input.readFully(extraData)

// Process the extra data request locally if needed
// This is where the multi-step flow continues
val localResult = processLocalStep(context, request, extraData, step)

// Send continuation
output.writeInt(DG_CMD_GET_RESULT_MULTI_CONTINUE)
output.writeInt(localResult.size)
output.write(localResult)
output.flush()
step++
}
2 -> { // Final result
val resultLength = input.readInt()
val result = ByteArray(resultLength)
input.readFully(result)
return@withContext DroidGuardResult(result)
}
3 -> { // Error
val errorLength = input.readInt()
val errorBytes = ByteArray(errorLength)
input.readFully(errorBytes)
throw IOException(String(errorBytes))
}
else -> throw IOException("Unknown response type: $responseType")
}
}
} else {
// Single step (original behavior)
output.writeInt(DG_CMD_GET_RESULT)
writeRequest(output, request)
output.flush()

// Read response
val resultLength = input.readInt()
val result = ByteArray(resultLength)
input.readFully(result)
return@withContext DroidGuardResult(result)
}
}
}

private fun processLocalStep(context: Context, request: DroidGuardRequest, extraData: ByteArray, step: Int): ByteArray {
// For Play Integrity, we may need to do local processing between steps
// This allows the remote server to request additional attestation data
// The default implementation just returns the extra data as-is
// Subclasses or future versions can override this behavior
return try {
// Try to extract any local state needed for the multi-step flow
// This is a placeholder for any local processing that might be needed
extraData
} catch (e: Exception) {
Log.w(TAG, "Error in local step processing", e)
extraData
}
}

output.writeInt(request.packageName?.length ?: 0)
output.write(request.packageName?.toByteArray() ?: ByteArray(0))

val requestBytes = request.toByteArray()
output.writeInt(requestBytes.size)
output.write(requestBytes)
183 changes: 183 additions & 0 deletions shanaboo_solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
```diff
--- a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardRemote.kt
+++ b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardRemote.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2024 microG Project Team
+ * Copyright (C) 2013-2025 microG Project Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.microg.gms.droidguard

import android.content.Context
+import android.util.Base64
import android.util.Log
import com.google.android.gms.droidguard.DroidGuardResultsRequest
import kotlinx.coroutines.Dispatchers
@@ -23,6 +24,7 @@
import kotlinx.coroutines.withContext
import org.microg.gms.common.PackageUtils
import org.microg.gms.common.GooglePackageUtils
+import org.microg.gms.utils.toBase64
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.io.IOException
@@ -33,6 +35,7 @@
import java.net.SocketTimeoutException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
+import java.util.UUID

private const val TAG = "DroidGuardRemote"

@@ -42,6 +45,9 @@
private val port: Int
private val useTls: Boolean

+ // Session state for multi-step DroidGuard flows (Play Integrity)
+ private var sessionState: MutableMap<String, ByteArray> = mutableMapOf()
+
init {
val parts = serverAddress.split(":")
this.host = parts[0]
@@ -49,7 +55,7 @@
this.useTls = host.endsWith(".onion") || port == 443
}

- suspend fun sendRequest(
+ private suspend fun sendSingleRequest(
context: Context,
flow: String,
packageName: String,
@@ -57,7 +63,7 @@
request: ByteArray?,
callback: DroidGuardCallback?
): ByteArray? = withContext(Dispatchers.IO) {
- Log.d(TAG, "Sending remote DroidGuard request for flow: $flow")
+ Log.d(TAG, "Sending remote DroidGuard request for flow: $flow, package: $packageName")

val socket = try {
if (useTls) {
@@ -77,7 +83,7 @@
return@withContext null
}

- val response = try {
+ try {
val output = DataOutputStream(socket.getOutputStream())
val input = socket.getInputStream()

@@ -86,7 +92,7 @@
val requestBytes = buildRequest(context, flow, packageName, packageSignature, request)

// Write length-prefixed request
- output.writeInt(requestBytes.size)
+ output.writeInt(requestBytes.size)
output.write(requestBytes)
output.flush()

@@ -97,7 +103,7 @@
val responseBytes = ByteArray(responseLength)
input.readFully(responseBytes)

- responseBytes
+ responseBytes
} catch (e: SocketTimeoutException) {
Log.w(TAG, "Remote DroidGuard request timed out")
null
@@ -106,9 +112,9 @@
null
} finally {
socket.close()
- }
-
- response
+ }
}

private fun buildRequest(
@@ -117,7 +123,8 @@
packageName: String,
packageSignature: String,
request: ByteArray?
- ): ByteArray {
+ ): ByteArray {
+ // Protocol version 2 supports multi-step flows with session state
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)

@@ -125,7 +132,7 @@
dos.writeInt(2) // Protocol version

// Write flow name
- val flowBytes = flow.toByteArray(Charsets.UTF_8)
+ val flowBytes = flow.toByteArray(Charsets.UTF_8)
dos.writeInt(flowBytes.size)
dos.write(flowBytes)

@@ -139,7 +146,7 @@
dos.writeInt(sigBytes.size)
dos.write(sigBytes)

- // Write request data
+ // Write request data (may contain session state for multi-step)
if (request != null) {
dos.writeInt(request.size)
dos.write(request)
@@ -150,6 +157,91 @@
return bos.toByteArray()
}

+ suspend fun sendRequest(
+ context: Context,
+ flow: String,
+ packageName: String,
+ packageSignature: String,
+ request: ByteArray?,
+ callback: DroidGuardCallback?
+ ): ByteArray? {
+ // Check if this is a multi-step flow (Play Integrity)
+ val isMultiStep = isMultiStepFlow(flow, request)
+
+ return if (isMultiStep) {
+ handleMultiStepRequest(context, flow, packageName, packageSignature, request, callback)
+ } else {
+ sendSingleRequest(context, flow, packageName, packageSignature, request, callback)
+ }
+ }
+
+ /**
+ * Detects if this is a multi-step DroidGuard flow used by Play Integrity.
+ * Play Integrity uses multiple DroidGuard requests with state carried between steps.
+ */
+ private fun isMultiStepFlow(flow: String, request: ByteArray?): Boolean {
+ // Play Integrity flows typically use specific flow names
+ val playIntegrityFlows = listOf("play_integrity", "integrity", "playintegrity")
+ val lowerFlow = flow.lowercase()
+
+ return playIntegrityFlows.any { lowerFlow.contains(it) } ||
+ // Also detect by request structure: multi-step has session continuation data
+ (request != null && request.size > 16 && hasSessionMarker(request))
+ }
+
+ /**
+ * Checks if request data contains session continuation markers.
+ */
+ private fun hasSessionMarker(request: ByteArray): Boolean {
+ // Look for session ID pattern or continuation flag in request
+ // This is a heuristic based on observed Play Integrity request structures
+ return request.size > 20 && request[0] == 0x08.toByte()
+ }
+
+ /**
+ * Handles multi-step DroidGuard requests by maintaining session state across steps.
+ * This is required for Play Integrity which uses a multi-step attestation process.
+ */
+ private suspend fun handleMultiStepRequest(
+ context: Context,
+ flow: String,
+ packageName: String,
+ packageSignature: String,
+ request: ByteArray
9 changes: 9 additions & 0 deletions solution.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Multi-step loop
byte[] currentRequest = request;
while (true) {
byte[] result = executeRemoteDroidGuard(currentRequest);
if (isFinalResult(result)) {
return result;
}
currentRequest = prepareNextStep(result);
}