Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
dependencies {
api project(':play-services-auth-workaccount')
api project(':play-services-auth')
implementation project(':play-services-base-core')
implementation "androidx.appcompat:appcompat:$appcompatVersion"
}
android {
namespace "org.microg.gms.auth.workaccount"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
lintOptions {
disable 'MissingTranslation'
}
}

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2023 e foundation
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<application>
<service android:name="org.microg.gms.auth.workaccount.WorkAccountService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.auth.account.workaccount.START" />
</intent-filter>
</service>
<service
android:name="com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService"
android:process=":persistent"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/auth_work_authenticator"/>
<meta-data
android:name="android.accounts.AccountAuthenticator.customTokens"
android:value="1"/>
</service>
</application>
</manifest>

View file

@ -0,0 +1,240 @@
/*
* SPDX-FileCopyrightText: 2024 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.account.authenticator
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.util.Log
import org.microg.gms.auth.workaccount.R
import org.microg.gms.auth.AuthConstants
import org.microg.gms.common.PackageUtils
import org.microg.gms.auth.AuthRequest
import org.microg.gms.auth.AuthResponse
import org.microg.gms.auth.workaccount.WorkProfileSettings
import java.io.IOException
import kotlin.jvm.Throws
class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String?
): Bundle {
TODO("Not yet implemented: editProperties")
}
override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String?,
requiredFeatures: Array<out String>?,
options: Bundle
): Bundle? {
if (!WorkProfileSettings(context).allowCreateWorkAccount) {
return Bundle().apply {
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_disabled_error)
)
}
} else if (
!options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)
|| options.getString(KEY_ACCOUNT_CREATION_TOKEN) == null
|| options.getInt(AccountManager.KEY_CALLER_UID) != android.os.Process.myUid()) {
Log.e(TAG,
"refusing to add account without creation token or from external app: " +
"could have been manually initiated by user (not supported) " +
"or by unauthorized app (not allowed)"
)
// TODO: The error message is not automatically displayed by the settings app as of now.
// We can consider showing the error message through a popup instead.
return Bundle().apply {
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_add_manual_error)
)
}
}
val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN)!!
try {
tryAddAccount(oauthToken, response)
} catch (exception: Exception) {
response.onResult(Bundle().apply {
putInt(
AccountManager.KEY_ERROR_CODE,
AccountManager.ERROR_CODE_NETWORK_ERROR
)
putString(AccountManager.KEY_ERROR_MESSAGE, exception.message)
})
}
/* Note: as is not documented, `null` must only be returned after `response.onResult` was
* already called, hence forcing the requests to be synchronous. They are still async to
* the caller's main thread because AccountManager forces potentially blocking operations,
* like waiting for a response upon `addAccount`, not to be on the main thread.
*/
return null
}
@Throws(Exception::class)
private fun tryAddAccount(
oauthToken: String,
response: AccountAuthenticatorResponse
) {
val authResponse = AuthRequest().fromContext(context)
.appIsGms()
.callerIsGms()
.service("ac2dm")
.token(oauthToken).isAccessToken()
.addAccount()
.getAccountId()
.droidguardResults(null)
.response
val accountManager = AccountManager.get(context)
if (accountManager.addAccountExplicitly(
Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE),
authResponse.token, Bundle().apply {
// Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name.
if (authResponse.accountId.isNotBlank()) {
putString(KEY_GOOGLE_USER_ID, authResponse.accountId)
}
putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities)
putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services)
if (authResponse.services != "android") {
Log.i(
TAG,
"unexpected 'services' value ${authResponse.services} (usually 'android')"
)
}
}
)
) {
// Notify vending package
context.sendBroadcast(
Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending")
)
// Report successful creation to caller
response.onResult(Bundle().apply {
putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email)
putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE)
})
}
}
override fun confirmCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
options: Bundle?
): Bundle {
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true)
}
}
override fun getAuthToken(
response: AccountAuthenticatorResponse?,
account: Account,
authTokenType: String?,
options: Bundle?
): Bundle {
try {
val authResponse: AuthResponse =
AuthRequest().fromContext(context)
.source("android")
.app(
context.packageName,
PackageUtils.firstSignatureDigest(context, context.packageName)
)
.email(account.name)
.token(AccountManager.get(context).getPassword(account))
.service(authTokenType)
.delegation(0, null)
// .oauth2Foreground(oauth2Foreground)
// .oauth2Prompt(oauth2Prompt)
// .oauth2IncludeProfile(includeProfile)
// .oauth2IncludeEmail(includeEmail)
// .itCaveatTypes(itCaveatTypes)
// .tokenRequestOptions(tokenRequestOptions)
.systemPartition(true)
.hasPermission(true)
// .putDynamicFiledMap(dynamicFields)
.appIsGms()
.callerIsApp()
.response
return Bundle().apply {
putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
putString(AccountManager.KEY_AUTHTOKEN, authResponse.auth)
}
} catch (e: IOException) {
return Bundle().apply {
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_NETWORK_ERROR)
putString(AccountManager.KEY_ERROR_MESSAGE, e.message)
}
}
}
override fun getAuthTokenLabel(authTokenType: String?): String {
TODO("Not yet implemented: getAuthTokenLabel")
}
override fun updateCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
TODO("Not yet implemented: updateCredentials")
}
override fun hasFeatures(
response: AccountAuthenticatorResponse?,
account: Account?,
features: Array<out String>
): Bundle {
Log.i(TAG, "Queried features: " + features.joinToString(", "))
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)
}
}
/**
* Prevent accidental deletion, unlike GMS. The account can only be removed through client apps;
* ideally, it would only be removed by the app that requested it to be created / the DPC
* manager, though this is not enforced. On API 21, the account can also be removed by hand
* because `removeAccountExplicitly` is not available on API 21.
*/
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse?,
account: Account?
): Bundle {
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, SDK_INT < 22)
}
}
companion object {
const val TAG = "WorkAccAuthenticator"
const val WORK_ACCOUNT_CHANGED_BOARDCAST = "org.microg.vending.WORK_ACCOUNT_CHANGED"
const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken"
private const val KEY_GOOGLE_USER_ID = AuthConstants.GOOGLE_USER_ID
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.auth.account.authenticator
import android.accounts.AccountManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
class WorkAccountAuthenticatorService : Service() {
private val authenticator by lazy { WorkAccountAuthenticator(this) }
override fun onBind(intent: Intent): IBinder? {
if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) {
return authenticator.iBinder
}
return null
}
}

View file

@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2024 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.workaccount
import android.accounts.Account
import android.accounts.AccountManager
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Parcel
import android.util.Log
import com.google.android.gms.auth.account.IWorkAccountCallback
import com.google.android.gms.auth.account.IWorkAccountService
import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.KEY_ACCOUNT_CREATION_TOKEN
import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_CHANGED_BOARDCAST
import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService
import com.google.android.gms.common.Feature
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.internal.ConnectionInfo
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.auth.AuthConstants
import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils
private const val TAG = "GmsWorkAccountService"
class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) {
override fun handleServiceRequest(
callback: IGmsCallbacks,
request: GetServiceRequest,
service: GmsService
) {
val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName)
val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val authorized = policyManager.isDeviceAdminApp(packageName)
if (authorized) {
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS,
WorkAccountServiceImpl(this),
ConnectionInfo().apply {
features = arrayOf(Feature("work_account_client_is_whitelisted", 1))
})
} else {
// Return mock response, don't tell client that it is whitelisted
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS,
UnauthorizedWorkAccountServiceImpl(),
ConnectionInfo().apply {
features = emptyArray()
})
}
}
}
private fun DevicePolicyManager.isDeviceAdminApp(packageName: String?): Boolean {
if (packageName == null) return false
return if (SDK_INT >= 21) {
isDeviceOwnerApp(packageName) || isProfileOwnerApp(packageName)
} else {
isDeviceOwnerApp(packageName)
}
}
class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() {
val packageManager: PackageManager = context.packageManager
val accountManager: AccountManager = AccountManager.get(context)
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
Log.d(TAG, "$code, $data, $reply, $flags")
return super.onTransact(code, data, reply, flags)
}
override fun setWorkAuthenticatorEnabled(enabled: Boolean) {
Log.d(TAG, "setWorkAuthenticatorEnabled with $enabled")
val componentName = ComponentName(
context,
WorkAccountAuthenticatorService::class.java
)
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
override fun addWorkAccount(
callback: IWorkAccountCallback?,
token: String?
) {
Log.d(TAG, "addWorkAccount with token $token")
val future = accountManager.addAccount(
AuthConstants.WORK_ACCOUNT_TYPE,
null,
null,
Bundle().apply { putString(KEY_ACCOUNT_CREATION_TOKEN, token) },
null,
null,
null
)
Thread {
try {
future.result.let { result ->
callback?.onAccountAdded(
Account(
result.getString(AccountManager.KEY_ACCOUNT_NAME)!!,
result.getString(AccountManager.KEY_ACCOUNT_TYPE)!!
)
)
}
} catch (e: Exception) {
Log.e(TAG, "could not add work account with error message: ${e.message}")
}
}.start()
}
override fun removeWorkAccount(
callback: IWorkAccountCallback?,
account: Account?
) {
Log.d(TAG, "removeWorkAccount with account ${account?.name}")
account?.let {
if (SDK_INT >= 22) {
val success = accountManager.removeAccountExplicitly(it)
// Notify vending package
context.sendBroadcast(
Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending")
)
callback?.onAccountRemoved(success)
} else {
val future = accountManager.removeAccount(it, null, null)
Thread {
future.result.let { result ->
callback?.onAccountRemoved(result)
}
}.start()
}
}
}
}
class UnauthorizedWorkAccountServiceImpl : IWorkAccountService.Stub() {
override fun setWorkAuthenticatorEnabled(enabled: Boolean) {
throw SecurityException("client not admin, yet tried to enable work authenticator")
}
override fun addWorkAccount(callback: IWorkAccountCallback?, token: String?) {
throw SecurityException("client not admin, yet tried to add work account")
}
override fun removeWorkAccount(callback: IWorkAccountCallback?, account: Account?) {
throw SecurityException("client not admin, yet tried to remove work account")
}
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth.workaccount
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.microg.gms.settings.SettingsContract
class WorkProfileSettings(private val context: Context) {
private fun <T> getSettings(vararg projection: String, f: (Cursor) -> T): T =
SettingsContract.getSettings(
context,
SettingsContract.WorkProfile.getContentUri(context),
projection,
f
)
private fun setSettings(v: ContentValues.() -> Unit) =
SettingsContract.setSettings(context, SettingsContract.WorkProfile.getContentUri(context), v)
var allowCreateWorkAccount: Boolean
get() = getSettings(SettingsContract.WorkProfile.CREATE_WORK_ACCOUNT) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.WorkProfile.CREATE_WORK_ACCOUNT, value) }
}

View file

@ -0,0 +1,9 @@
<!--
~ SPDX-FileCopyrightText: Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#4285F4" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z"/>
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2024 e foundation
~ SPDX-License-Identifier: Apache-2.0
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="auth_work_authenticator_label">Managed Google for Work account</string>
<string name="auth_work_authenticator_add_manual_error">This type of account is created automatically by your profile administrator if needed.</string>
<string name="auth_work_authenticator_disabled_error">Creating a work account is disabled in microG settings.</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2024 e foundation
~ SPDX-License-Identifier: Apache-2.0
-->
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/auth_work_authenticator_label"
android:icon="@drawable/ic_briefcase"
android:accountType="com.google.work"
android:smallIcon="@drawable/ic_briefcase"
android:customTokens="true"/>