Add new Auth API features

This commit is contained in:
Marvin W 2022-01-18 18:54:05 +01:00
parent 854f879da4
commit 10de88b89f
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
13 changed files with 418 additions and 5 deletions

View File

@ -0,0 +1,8 @@
package com.google.android.gms.auth.api.internal;
import com.google.android.gms.auth.api.proxy.ProxyResponse;
interface IAuthCallbacks {
void onProxyResponse(in ProxyResponse response) = 0;
void onSpatulaHeader(String spatulaHeader) = 1;
}

View File

@ -0,0 +1,11 @@
package com.google.android.gms.auth.api.internal;
import com.google.android.gms.auth.api.internal.IAuthCallbacks;
//import com.google.android.gms.auth.api.proxy.ProxyGrpcRequest;
import com.google.android.gms.auth.api.proxy.ProxyRequest;
interface IAuthService {
void performProxyRequest(IAuthCallbacks callbacks, in ProxyRequest request) = 0;
// void performProxyGrpcRequest(IAuthCallback callbacks, in ProxyGrpcRequest request) = 1;
void getSpatulaHeader(IAuthCallbacks callbacks) = 2;
}

View File

@ -0,0 +1,3 @@
package com.google.android.gms.auth.api.proxy;
parcelable ProxyRequest;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.auth.api.proxy;
parcelable ProxyResponse;

View File

@ -0,0 +1,6 @@
package com.google.android.gms.auth.appcert;
interface IAppCertService {
boolean fetchDeviceKey() = 0;
String getSpatulaHeader(String packageName) = 1;
}

View File

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.api.proxy;
import android.os.Bundle;
import org.microg.safeparcel.AutoSafeParcelable;
public class ProxyRequest extends AutoSafeParcelable {
public static final int HTTP_METHOD_GET = 0;
public static final int HTTP_METHOD_POST = 1;
public static final int HTTP_METHOD_PUT = 2;
public static final int HTTP_METHOD_DELETE = 3;
public static final int HTTP_METHOD_HEAD = 4;
public static final int HTTP_METHOD_OPTIONS = 5;
public static final int HTTP_METHOD_TRACE = 6;
public static final int HTTP_METHOD_PATCH = 7;
@Field(1000)
private int versionCode = 2;
@Field(1)
public String url;
@Field(2)
public int httpMethod;
@Field(3)
public long timeoutMillis;
@Field(4)
public byte[] body;
@Field(5)
public Bundle headers;
@Override
public String toString() {
return url;
}
public static final Creator<ProxyRequest> CREATOR = new AutoCreator<>(ProxyRequest.class);
}

View File

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.api.proxy;
import android.app.PendingIntent;
import android.os.Bundle;
import org.microg.safeparcel.AutoSafeParcelable;
public class ProxyResponse extends AutoSafeParcelable {
public static final int STATUS_CODE_NO_CONNECTION = -1;
@Field(1000)
private int versionCode = 1;
@Field(1)
public int gmsStatusCode;
@Field(2)
public PendingIntent recoveryAction;
@Field(3)
public int httpStatusCode;
@Field(4)
public Bundle headers;
@Field(5)
public byte[] body;
public static final Creator<ProxyResponse> CREATOR = new AutoCreator<>(ProxyResponse.class);
}

View File

@ -0,0 +1,33 @@
option java_package = "org.microg.gms.auth.appcert";
option java_outer_classname = "AppCertProto";
message DeviceKeyRequest {
optional string droidGuardResult = 1;
optional uint64 androidId = 2;
optional uint64 sessionId = 3;
message VersionInfo {
optional uint32 sdkVersion = 1;
optional uint32 gmsVersion = 2;
}
optional VersionInfo versionInfo = 4;
optional string token = 5;
}
message DeviceKey {
optional uint64 keyId = 1;
optional uint64 deviceId = 3;
optional bytes macSecret = 4;
optional bytes keyCert = 5;
}
message SpatulaHeaderProto {
message PackageInfo {
optional string packageName = 1;
optional string packageCertificateHash = 3;
}
optional PackageInfo packageInfo = 1;
optional bytes hmac = 2;
optional uint64 deviceId = 3;
optional uint64 keyId = 4;
optional bytes keyCert = 5;
}

View File

@ -0,0 +1,170 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.appcert
import android.content.Context
import android.database.Cursor
import android.os.SystemClock
import android.util.Base64
import android.util.Log
import com.android.volley.NetworkResponse
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.Volley
import com.google.android.gms.BuildConfig
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okio.ByteString.Companion.of
import org.microg.gms.checkin.LastCheckinInfo
import org.microg.gms.common.Constants
import org.microg.gms.common.PackageUtils
import org.microg.gms.droidguard.core.DroidGuardResultCreator
import org.microg.gms.gcm.GcmConstants
import org.microg.gms.gcm.GcmDatabase
import org.microg.gms.gcm.RegisterRequest
import org.microg.gms.gcm.completeRegisterRequest
import org.microg.gms.profile.Build
import org.microg.gms.profile.ProfileManager
import org.microg.gms.settings.SettingsContract.CheckIn
import org.microg.gms.settings.SettingsContract.getSettings
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
class AppCertManager(private val context: Context) {
private val queue = Volley.newRequestQueue(context)
suspend fun fetchDeviceKey(): Boolean {
ProfileManager.ensureInitialized(context)
deviceKeyLock.withLock {
try {
val elapsedRealtime = SystemClock.elapsedRealtime()
if (elapsedRealtime - deviceKeyCacheTime < DEVICE_KEY_TIMEOUT) {
return deviceKey != null
}
Log.w(TAG, "DeviceKeys for app certifications are experimental")
deviceKeyCacheTime = elapsedRealtime
val lastCheckinInfo = LastCheckinInfo.read(context)
val androidId = lastCheckinInfo.androidId
val sessionId = Random.nextLong()
val data = hashMapOf(
"dg_androidId" to androidId.toString(16),
"dg_session" to sessionId.toString(16),
"dg_gmsCoreVersion" to BuildConfig.VERSION_CODE.toString(),
"dg_sdkVersion" to Build.VERSION.SDK_INT.toString()
)
val droidGuardResult = try {
Base64.encodeToString(DroidGuardResultCreator.getResult(context, "devicekey", data), Base64.NO_WRAP)
} catch (e: Exception) {
null
}
val token = completeRegisterRequest(context, GcmDatabase(context), RegisterRequest().build(context)
.checkin(lastCheckinInfo)
.app("com.google.android.gms", Constants.GMS_PACKAGE_SIGNATURE_SHA1, BuildConfig.VERSION_CODE)
.sender(REGISTER_SENDER)
.extraParam("subscription", REGISTER_SUBSCIPTION)
.extraParam("X-subscription", REGISTER_SUBSCIPTION)
.extraParam("subtype", REGISTER_SUBTYPE)
.extraParam("X-subtype", REGISTER_SUBTYPE)
.extraParam("scope", REGISTER_SCOPE))
.getString(GcmConstants.EXTRA_REGISTRATION_ID)
val request = DeviceKeyRequest(
droidGuardResult = droidGuardResult,
androidId = lastCheckinInfo.androidId,
sessionId = sessionId,
versionInfo = DeviceKeyRequest.VersionInfo(Build.VERSION.SDK_INT, BuildConfig.VERSION_CODE),
token = token
)
Log.d(TAG, "Request: ${request.toString().chunked(128).joinToString("\n")}")
val deferredResponse = CompletableDeferred<ByteArray?>()
queue.add(object : Request<ByteArray?>(Method.POST, "https://android.googleapis.com/auth/devicekey", null) {
override fun getBody(): ByteArray = request.encode()
override fun getBodyContentType(): String = "application/octet-stream"
override fun parseNetworkResponse(response: NetworkResponse): Response<ByteArray?> {
return if (response.statusCode == 200) {
Response.success(response.data, null)
} else {
Response.success(null, null)
}
}
override fun deliverError(error: VolleyError) {
Log.d(TAG, "Error: ${Base64.encodeToString(error.networkResponse.data, 2)}")
deferredResponse.complete(null)
}
override fun deliverResponse(response: ByteArray?) {
deferredResponse.complete(response)
}
override fun getHeaders(): Map<String, String> {
return mapOf(
"User-Agent" to "GoogleAuth/1.4 (${Build.DEVICE} ${Build.ID}); gzip",
"content-type" to "application/octet-stream",
"app" to "com.google.android.gms",
"device" to androidId.toString(16)
)
}
})
val deviceKeyBytes = deferredResponse.await() ?: return false
deviceKey = DeviceKey.ADAPTER.decode(deviceKeyBytes)
Log.d(TAG, "Response: $deviceKey")
return true
} catch (e: Exception) {
Log.w(TAG, e)
return false
}
}
}
suspend fun getSpatulaHeader(packageName: String): String? {
val deviceKey = deviceKey ?: if (fetchDeviceKey()) deviceKey else null
val packageCertificateHash = Base64.encodeToString(PackageUtils.firstSignatureDigestBytes(context, packageName), Base64.NO_WRAP)
val proto = if (deviceKey != null) {
val macSecret = deviceKey.macSecret?.toByteArray()
if (macSecret == null) {
Log.w(TAG, "Invalid device key: $deviceKey")
return null
}
val mac = Mac.getInstance("HMACSHA256")
mac.init(SecretKeySpec(macSecret, "HMACSHA256"))
val hmac = mac.doFinal("$packageName$packageCertificateHash".toByteArray())
SpatulaHeaderProto(
packageInfo = SpatulaHeaderProto.PackageInfo(packageName, packageCertificateHash),
hmac = of(*hmac),
deviceId = deviceKey.deviceId,
keyId = deviceKey.keyId,
keyCert = deviceKey.keyCert ?: of()
)
} else {
Log.d(TAG, "Using fallback spatula header based on Android ID")
val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID, CheckIn.SECURITY_TOKEN)) { cursor: Cursor -> cursor.getLong(0) }
SpatulaHeaderProto(
packageInfo = SpatulaHeaderProto.PackageInfo(packageName, packageCertificateHash),
deviceId = androidId
)
}
Log.d(TAG, "Spatula Header: $proto")
return Base64.encodeToString(proto.encode(), Base64.NO_WRAP)
}
companion object {
private const val TAG = "AppCertManager"
private const val DEVICE_KEY_TIMEOUT = 60 * 60 * 1000L
private const val REGISTER_SENDER = "745476177629"
private const val REGISTER_SUBTYPE = "745476177629"
private const val REGISTER_SUBSCIPTION = "745476177629"
private const val REGISTER_SCOPE = "DeviceKeyRequest"
private val deviceKeyLock = Mutex()
private var deviceKey: DeviceKey? = null
private var deviceKeyCacheTime = 0L
}
}

View File

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.appcert
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.Parcel
import android.util.Log
import com.google.android.gms.auth.appcert.IAppCertService
import kotlinx.coroutines.runBlocking
import org.microg.gms.common.PackageUtils
import org.microg.gms.utils.warnOnTransactionIssues
private const val TAG = "AppCertService"
class AppCertService : Service() {
override fun onBind(intent: Intent): IBinder {
Log.d(TAG, "onBind: $intent")
return AppCertServiceImpl(this).asBinder()
}
}
class AppCertServiceImpl(private val context: Context) : IAppCertService.Stub() {
private val manager = AppCertManager(context)
override fun fetchDeviceKey(): Boolean {
PackageUtils.assertExtendedAccess(context)
return runBlocking { manager.fetchDeviceKey() }
}
override fun getSpatulaHeader(packageName: String): String? {
PackageUtils.assertExtendedAccess(context)
return runBlocking { manager.getSpatulaHeader(packageName) }
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) }
}

View File

@ -1,9 +1,9 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth
package org.microg.gms.auth.credentials
import android.app.Activity
import android.os.Bundle

View File

@ -1,11 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth
package org.microg.gms.auth.credentials
import android.os.Bundle
import android.os.Parcel
import android.util.Log
import com.google.android.gms.auth.api.credentials.CredentialRequest
import com.google.android.gms.auth.api.credentials.internal.*
@ -15,8 +16,9 @@ import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.utils.warnOnTransactionIssues
const val TAG = "GmsCredentials"
private const val TAG = "CredentialService"
class CredentialsService : BaseService(TAG, GmsService.CREDENTIALS) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
@ -50,4 +52,5 @@ class CredentialsServiceImpl : ICredentialsService.Stub() {
callbacks.onStatus(Status.SUCCESS)
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) }
}

View File

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.proxy
import android.content.Context
import android.os.Bundle
import android.os.Parcel
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.auth.api.internal.IAuthCallbacks
import com.google.android.gms.auth.api.internal.IAuthService
import com.google.android.gms.auth.api.proxy.ProxyRequest
import com.google.android.gms.auth.api.proxy.ProxyResponse
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.BaseService
import org.microg.gms.auth.appcert.AppCertManager
import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils
import org.microg.gms.utils.warnOnTransactionIssues
private const val TAG = "AuthProxyService"
class AuthProxyService : BaseService(TAG, GmsService.AUTH_PROXY) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName)
?: throw IllegalArgumentException("Missing package name")
val consumerPackageName = request.extras.getString("consumerPkg")
if (consumerPackageName != null) PackageUtils.assertExtendedAccess(this)
val serviceImpl = AuthServiceImpl(this, lifecycle, consumerPackageName ?: packageName)
callback.onPostInitComplete(CommonStatusCodes.SUCCESS, serviceImpl, Bundle())
}
}
class AuthServiceImpl(private val context: Context, private val lifecycle: Lifecycle, private val packageName: String) : IAuthService.Stub(), LifecycleOwner {
override fun performProxyRequest(callbacks: IAuthCallbacks, request: ProxyRequest) {
Log.d(TAG, "performProxyRequest($packageName, $request)")
lifecycleScope.launchWhenStarted {
callbacks.onProxyResponse(ProxyResponse().apply { gmsStatusCode = CommonStatusCodes.CANCELED })
}
}
override fun getSpatulaHeader(callbacks: IAuthCallbacks) {
Log.d(TAG, "getSpatulaHeader($packageName)")
lifecycleScope.launchWhenStarted {
val result = withContext(Dispatchers.IO) { AppCertManager(context).getSpatulaHeader(packageName) }
Log.d(TAG, "Result: $result")
callbacks.onSpatulaHeader(result)
}
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) }
}