Loading play-services-core/src/main/AndroidManifest.xml +1 −1 Original line number Diff line number Diff line Loading @@ -367,7 +367,7 @@ <activity android:name="org.microg.gms.auth.login.LoginActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|keyboard|orientation|screenSize" android:exported="true" android:process=":ui" android:theme="@style/Theme.LoginBlue"> Loading play-services-core/src/main/java/org/microg/gms/auth/login/AssistantActivity.java +2 −1 Original line number Diff line number Diff line Loading @@ -27,10 +27,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import com.google.android.gms.R; public abstract class AssistantActivity extends Activity { public abstract class AssistantActivity extends AppCompatActivity { private static final int TITLE_MIN_HEIGHT = 64; private static final double TITLE_WIDTH_FACTOR = (8.0 / 18.0); Loading play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java +98 −5 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.view.KeyEvent; import android.view.View; Loading @@ -40,6 +42,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.StringRes; import androidx.core.app.OnNewIntentProvider; import androidx.webkit.WebViewClientCompat; import com.google.android.gms.R; Loading @@ -53,11 +56,14 @@ import org.microg.gms.checkin.CheckinManager; import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.common.HttpFormClient; import org.microg.gms.common.Utils; import org.microg.gms.droidguard.core.DroidGuardResultCreator; import org.microg.gms.people.PeopleManager; import org.microg.gms.profile.Build; import org.microg.gms.profile.ProfileManager; import java.io.IOException; import java.security.MessageDigest; import java.util.Collections; import java.util.Locale; import static android.accounts.AccountManager.PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE; Loading Loading @@ -89,6 +95,9 @@ public class LoginActivity extends AssistantActivity { private static final String MAGIC_USER_AGENT = " MinuteMaid"; private static final String COOKIE_OAUTH_TOKEN = "oauth_token"; private final FidoHandler fidoHandler = new FidoHandler(this); private final DroidGuardHandler dgHandler = new DroidGuardHandler(this); private WebView webView; private String accountType; private AccountManager accountManager; Loading Loading @@ -257,6 +266,10 @@ public class LoginActivity extends AssistantActivity { webView.loadUrl(buildUrl(tmpl, Utils.getLocale(this))); } protected void runScript(String js) { runOnUiThread(() -> webView.loadUrl("javascript:" + js)); } private void closeWeb(boolean programmaticAuth) { setMessage(R.string.auth_finalize); runOnUiThread(() -> webView.setVisibility(INVISIBLE)); Loading Loading @@ -394,12 +407,38 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: addAccount"); } @JavascriptInterface public final void attemptLogin(String accountName, String password) { Log.d(TAG, "JSBridge: attemptLogin"); } @JavascriptInterface public void backupSyncOptIn(String accountName) { Log.d(TAG, "JSBridge: backupSyncOptIn"); } @JavascriptInterface public final void cancelFido2SignRequest() { Log.d(TAG, "JSBridge: cancelFido2SignRequest"); fidoHandler.cancel(); } @JavascriptInterface public void clearOldLoginAttempts() { Log.d(TAG, "JSBridge: clearOldLoginAttempts"); } @JavascriptInterface public final void closeView() { Log.d(TAG, "JSBridge: closeView"); closeWeb(false); } @JavascriptInterface public void fetchIIDToken(String entity) { Log.d(TAG, "JSBridge: fetchIIDToken"); } @JavascriptInterface public final String fetchVerifiedPhoneNumber() { Log.d(TAG, "JSBridge: fetchVerifiedPhoneNumber"); Loading Loading @@ -434,17 +473,17 @@ public class LoginActivity extends AssistantActivity { @JavascriptInterface public final int getAuthModuleVersionCode() { return 1; return GMS_VERSION_CODE; } @JavascriptInterface public final int getBuildVersionSdk() { return SDK_INT; return Build.VERSION.SDK_INT; } @JavascriptInterface public final void getDroidGuardResult(String s) { Log.d(TAG, "JSBridge: getDroidGuardResult"); public int getDeviceContactsCount() { return -1; } @JavascriptInterface Loading @@ -452,6 +491,23 @@ public class LoginActivity extends AssistantActivity { return 1; } @JavascriptInterface public final void getDroidGuardResult(String s) { Log.d(TAG, "JSBridge: getDroidGuardResult"); try { JSONArray array = new JSONArray(s); StringBuilder sb = new StringBuilder(); sb.append(getAndroidId()).append(":").append(getBuildVersionSdk()).append(":").append(getPlayServicesVersionCode()); for (int i = 0; i < array.length(); i++) { sb.append(":").append(array.getString(i)); } String dg = Base64.encodeToString(MessageDigest.getInstance("SHA1").digest(sb.toString().getBytes()), 0); dgHandler.start(dg); } catch (Exception e) { // Ignore } } @JavascriptInterface public final String getFactoryResetChallenges() { return new JSONArray().toString(); Loading Loading @@ -518,10 +574,21 @@ public class LoginActivity extends AssistantActivity { } @JavascriptInterface public final void setAccountIdentifier(String accountIdentifier) { public final void sendFido2SkUiEvent(String event) { Log.d(TAG, "JSBridge: sendFido2SkUiEvent"); fidoHandler.onEvent(event); } @JavascriptInterface public final void setAccountIdentifier(String accountName) { Log.d(TAG, "JSBridge: setAccountIdentifier"); } @JavascriptInterface public void setAllActionsEnabled(boolean z) { Log.d(TAG, "JSBridge: setAllActionsEnabled"); } @TargetApi(HONEYCOMB) @JavascriptInterface public final void setBackButtonEnabled(boolean backButtonEnabled) { Loading @@ -540,6 +607,26 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: setNewAccountCreated"); } @JavascriptInterface public void setPrimaryActionEnabled(boolean z) { Log.d(TAG, "JSBridge: setPrimaryActionEnabled"); } @JavascriptInterface public void setPrimaryActionLabel(String str, int i) { Log.d(TAG, "JSBridge: setPrimaryActionLabel: " + str); } @JavascriptInterface public void setSecondaryActionEnabled(boolean z) { Log.d(TAG, "JSBridge: setSecondaryActionEnabled"); } @JavascriptInterface public void setSecondaryActionLabel(String str, int i) { Log.d(TAG, "JSBridge: setSecondaryActionLabel: " + str); } @JavascriptInterface public final void showKeyboard() { inputMethodManager.showSoftInput(webView, SHOW_IMPLICIT); Loading @@ -561,5 +648,11 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: startAfw"); } @JavascriptInterface public final void startFido2SignRequest(String request) { Log.d(TAG, "JSBridge: startFido2SignRequest"); fidoHandler.startSignRequest(request); } } } play-services-core/src/main/kotlin/org/microg/gms/auth/login/DroidGuardHandler.kt 0 → 100644 +22 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.auth.login import android.util.Base64 import androidx.lifecycle.lifecycleScope import org.microg.gms.droidguard.core.DroidGuardResultCreator.Companion.getResult import org.microg.gms.utils.toBase64 import java.util.* class DroidGuardHandler(private val activity: LoginActivity) { fun start(dg: String) { activity.lifecycleScope.launchWhenStarted { val result = getResult(activity, "minute_maid", Collections.singletonMap("dg_minutemaid", dg)) .toBase64(Base64.NO_WRAP, Base64.NO_PADDING, Base64.URL_SAFE) activity.runScript("window.setDgResult('$result')"); } } } play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.auth.login import android.os.Build import android.os.Bundle import android.util.Base64 import android.util.Log import androidx.lifecycle.lifecycleScope import com.google.android.gms.fido.fido2.api.common.* import kotlinx.coroutines.CancellationException import org.json.JSONArray import org.json.JSONObject import org.microg.gms.fido.core.RequestHandlingException import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandlerCallback import org.microg.gms.fido.core.transport.bluetooth.BluetoothTransportHandler import org.microg.gms.fido.core.transport.nfc.NfcTransportHandler import org.microg.gms.fido.core.transport.screenlock.ScreenLockTransportHandler import org.microg.gms.fido.core.transport.usb.UsbTransportHandler import org.microg.gms.utils.toBase64 fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null fun JSONObject.getIntOrNull(key: String) = if (has(key)) getInt(key) else null fun JSONObject.getDoubleOrNull(key: String) = if (has(key)) getDouble(key) else null fun JSONObject.getArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallback { private lateinit var requestOptions: PublicKeyCredentialRequestOptions private val transportHandlers by lazy { setOfNotNull( BluetoothTransportHandler(activity, this), NfcTransportHandler(activity, this), if (Build.VERSION.SDK_INT >= 21) UsbTransportHandler(activity, this) else null, if (Build.VERSION.SDK_INT >= 23) ScreenLockTransportHandler(activity, this) else null ) } override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) { Log.d(TAG, "onStatusChanged: $transport, $status") } private fun sendEvent(type: String, data: JSONObject, extras: JSONObject? = null) { val event = JSONObject(extras?.toString() ?: "{}") event.put("type", type) event.put("data", data) activity.runScript("window.setFido2SkUiEvent($event)") } private fun sendResult(result: JSONObject) { activity.runScript("window.setFido2SkResult($result)") } private fun sendSelectView(viewName: String, extras: JSONObject? = null) { val data = JSONObject(extras?.toString() ?: "{}") data.put("viewName", viewName) sendEvent("select_view", data) } private fun sendErrorResult(errorCode: ErrorCode, errorMessage: String?) { Log.d(TAG, "Finish with error: $errorMessage ($errorCode)") sendResult(JSONObject().apply { put("errorCode", errorCode.code) if (errorMessage != null) put("errorMessage", errorMessage) }) } private fun sendSuccessResult(response: AuthenticatorResponse, transport: Transport) { Log.d(TAG, "Finish with success response: $response") if (response is AuthenticatorAssertionResponse) { sendResult(JSONObject().apply { val base64Flags = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE put("keyHandle", response.keyHandle?.toBase64(base64Flags)) put("clientDataJSON", response.clientDataJSON?.toBase64(base64Flags)) put("authenticatorData", response.authenticatorData?.toBase64(base64Flags)) put("signature", response.signature?.toBase64(base64Flags)) if (response.userHandle != null) { put("userHandle", response.userHandle?.toBase64(base64Flags)) } }) } } private val availableTransports: List<String> get() { val list = mutableListOf<String>() val transports = transportHandlers.filter { it.isSupported }.map { it.transport } if (Transport.BLUETOOTH in transports) { list.add("bt") list.add("ble") } if (Transport.USB in transports) list.add("usb") if (Transport.NFC in transports) list.add("nfc") if (Transport.SCREEN_LOCK in transports) list.add("internal") return list } fun startSignRequest(request: String) { try { val requestObject = JSONObject(request) requestOptions = PublicKeyCredentialRequestOptions.Builder().apply { val base64Flags = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE requestObject.getStringOrNull("challenge")?.let { setChallenge(Base64.decode(it, base64Flags)) } requestObject.getDoubleOrNull("timeoutSeconds")?.let { setTimeoutSeconds(it) } requestObject.getStringOrNull("rpId")?.let { setRpId(it) } requestObject.getArrayOrNull("allowList")?.let { val allowList = mutableListOf<PublicKeyCredentialDescriptor>() for (i in 0 until it.length()) { val obj = it.getJSONObject(i) allowList.add( PublicKeyCredentialDescriptor( obj.getStringOrNull("type") ?: "public-key", Base64.decode(obj.getString("id"), base64Flags), emptyList() ) ) } setAllowList(allowList) } requestObject.getIntOrNull("requestId")?.let { setRequestId(it) } }.build() Log.d(TAG, "sign: $requestOptions") sendSelectView("multiple_transports", JSONObject().apply { put("transports", JSONArray(availableTransports)) }) } catch (e: Exception) { Log.w(TAG, e) } } fun onEvent(event: String) { try { val eventObject = JSONObject(event) Log.d(TAG, "event: $eventObject") when (eventObject.getString("type")) { "user_selected_view_for_transport" -> { val transport = when (eventObject.getJSONObject("data").getString("transport")) { "bt" -> Transport.BLUETOOTH "ble" -> Transport.BLUETOOTH "nfc" -> Transport.NFC "usb" -> Transport.USB "internal" -> Transport.SCREEN_LOCK else -> return } val transportHandler = transportHandlers.firstOrNull { it.transport == transport && it.isSupported } ?: return activity.lifecycleScope.launchWhenStarted { val options = requestOptions try { sendSuccessResult(transportHandler.start(options, activity.packageName), transport) } catch (e: CancellationException) { Log.w(TAG, e) // Ignoring cancellation here } catch (e: RequestHandlingException) { Log.w(TAG, e) sendErrorResult(e.errorCode, e.message) } catch (e: Exception) { Log.w(TAG, e) sendErrorResult(ErrorCode.UNKNOWN_ERR, e.message) } } val extras = JSONObject().apply { put("alternateAvailableTransports", JSONArray(availableTransports)) } val viewName = when (transport) { Transport.NFC -> { extras.put("deviceRemovedTooSoon", false) extras.put("recommendUsb", false) "nfc_instructions" } Transport.USB -> "usb_instructions" Transport.BLUETOOTH -> "ble_instructions" else -> return } sendSelectView(viewName, extras) } } } catch (e: Exception) { Log.w(TAG, e) } } fun cancel() { Log.d(TAG, "cancel") } companion object { private const val TAG = "AuthFidoHandler" } } Loading
play-services-core/src/main/AndroidManifest.xml +1 −1 Original line number Diff line number Diff line Loading @@ -367,7 +367,7 @@ <activity android:name="org.microg.gms.auth.login.LoginActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|keyboard|orientation|screenSize" android:exported="true" android:process=":ui" android:theme="@style/Theme.LoginBlue"> Loading
play-services-core/src/main/java/org/microg/gms/auth/login/AssistantActivity.java +2 −1 Original line number Diff line number Diff line Loading @@ -27,10 +27,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import com.google.android.gms.R; public abstract class AssistantActivity extends Activity { public abstract class AssistantActivity extends AppCompatActivity { private static final int TITLE_MIN_HEIGHT = 64; private static final double TITLE_WIDTH_FACTOR = (8.0 / 18.0); Loading
play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java +98 −5 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.view.KeyEvent; import android.view.View; Loading @@ -40,6 +42,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.StringRes; import androidx.core.app.OnNewIntentProvider; import androidx.webkit.WebViewClientCompat; import com.google.android.gms.R; Loading @@ -53,11 +56,14 @@ import org.microg.gms.checkin.CheckinManager; import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.common.HttpFormClient; import org.microg.gms.common.Utils; import org.microg.gms.droidguard.core.DroidGuardResultCreator; import org.microg.gms.people.PeopleManager; import org.microg.gms.profile.Build; import org.microg.gms.profile.ProfileManager; import java.io.IOException; import java.security.MessageDigest; import java.util.Collections; import java.util.Locale; import static android.accounts.AccountManager.PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE; Loading Loading @@ -89,6 +95,9 @@ public class LoginActivity extends AssistantActivity { private static final String MAGIC_USER_AGENT = " MinuteMaid"; private static final String COOKIE_OAUTH_TOKEN = "oauth_token"; private final FidoHandler fidoHandler = new FidoHandler(this); private final DroidGuardHandler dgHandler = new DroidGuardHandler(this); private WebView webView; private String accountType; private AccountManager accountManager; Loading Loading @@ -257,6 +266,10 @@ public class LoginActivity extends AssistantActivity { webView.loadUrl(buildUrl(tmpl, Utils.getLocale(this))); } protected void runScript(String js) { runOnUiThread(() -> webView.loadUrl("javascript:" + js)); } private void closeWeb(boolean programmaticAuth) { setMessage(R.string.auth_finalize); runOnUiThread(() -> webView.setVisibility(INVISIBLE)); Loading Loading @@ -394,12 +407,38 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: addAccount"); } @JavascriptInterface public final void attemptLogin(String accountName, String password) { Log.d(TAG, "JSBridge: attemptLogin"); } @JavascriptInterface public void backupSyncOptIn(String accountName) { Log.d(TAG, "JSBridge: backupSyncOptIn"); } @JavascriptInterface public final void cancelFido2SignRequest() { Log.d(TAG, "JSBridge: cancelFido2SignRequest"); fidoHandler.cancel(); } @JavascriptInterface public void clearOldLoginAttempts() { Log.d(TAG, "JSBridge: clearOldLoginAttempts"); } @JavascriptInterface public final void closeView() { Log.d(TAG, "JSBridge: closeView"); closeWeb(false); } @JavascriptInterface public void fetchIIDToken(String entity) { Log.d(TAG, "JSBridge: fetchIIDToken"); } @JavascriptInterface public final String fetchVerifiedPhoneNumber() { Log.d(TAG, "JSBridge: fetchVerifiedPhoneNumber"); Loading Loading @@ -434,17 +473,17 @@ public class LoginActivity extends AssistantActivity { @JavascriptInterface public final int getAuthModuleVersionCode() { return 1; return GMS_VERSION_CODE; } @JavascriptInterface public final int getBuildVersionSdk() { return SDK_INT; return Build.VERSION.SDK_INT; } @JavascriptInterface public final void getDroidGuardResult(String s) { Log.d(TAG, "JSBridge: getDroidGuardResult"); public int getDeviceContactsCount() { return -1; } @JavascriptInterface Loading @@ -452,6 +491,23 @@ public class LoginActivity extends AssistantActivity { return 1; } @JavascriptInterface public final void getDroidGuardResult(String s) { Log.d(TAG, "JSBridge: getDroidGuardResult"); try { JSONArray array = new JSONArray(s); StringBuilder sb = new StringBuilder(); sb.append(getAndroidId()).append(":").append(getBuildVersionSdk()).append(":").append(getPlayServicesVersionCode()); for (int i = 0; i < array.length(); i++) { sb.append(":").append(array.getString(i)); } String dg = Base64.encodeToString(MessageDigest.getInstance("SHA1").digest(sb.toString().getBytes()), 0); dgHandler.start(dg); } catch (Exception e) { // Ignore } } @JavascriptInterface public final String getFactoryResetChallenges() { return new JSONArray().toString(); Loading Loading @@ -518,10 +574,21 @@ public class LoginActivity extends AssistantActivity { } @JavascriptInterface public final void setAccountIdentifier(String accountIdentifier) { public final void sendFido2SkUiEvent(String event) { Log.d(TAG, "JSBridge: sendFido2SkUiEvent"); fidoHandler.onEvent(event); } @JavascriptInterface public final void setAccountIdentifier(String accountName) { Log.d(TAG, "JSBridge: setAccountIdentifier"); } @JavascriptInterface public void setAllActionsEnabled(boolean z) { Log.d(TAG, "JSBridge: setAllActionsEnabled"); } @TargetApi(HONEYCOMB) @JavascriptInterface public final void setBackButtonEnabled(boolean backButtonEnabled) { Loading @@ -540,6 +607,26 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: setNewAccountCreated"); } @JavascriptInterface public void setPrimaryActionEnabled(boolean z) { Log.d(TAG, "JSBridge: setPrimaryActionEnabled"); } @JavascriptInterface public void setPrimaryActionLabel(String str, int i) { Log.d(TAG, "JSBridge: setPrimaryActionLabel: " + str); } @JavascriptInterface public void setSecondaryActionEnabled(boolean z) { Log.d(TAG, "JSBridge: setSecondaryActionEnabled"); } @JavascriptInterface public void setSecondaryActionLabel(String str, int i) { Log.d(TAG, "JSBridge: setSecondaryActionLabel: " + str); } @JavascriptInterface public final void showKeyboard() { inputMethodManager.showSoftInput(webView, SHOW_IMPLICIT); Loading @@ -561,5 +648,11 @@ public class LoginActivity extends AssistantActivity { Log.d(TAG, "JSBridge: startAfw"); } @JavascriptInterface public final void startFido2SignRequest(String request) { Log.d(TAG, "JSBridge: startFido2SignRequest"); fidoHandler.startSignRequest(request); } } }
play-services-core/src/main/kotlin/org/microg/gms/auth/login/DroidGuardHandler.kt 0 → 100644 +22 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.auth.login import android.util.Base64 import androidx.lifecycle.lifecycleScope import org.microg.gms.droidguard.core.DroidGuardResultCreator.Companion.getResult import org.microg.gms.utils.toBase64 import java.util.* class DroidGuardHandler(private val activity: LoginActivity) { fun start(dg: String) { activity.lifecycleScope.launchWhenStarted { val result = getResult(activity, "minute_maid", Collections.singletonMap("dg_minutemaid", dg)) .toBase64(Base64.NO_WRAP, Base64.NO_PADDING, Base64.URL_SAFE) activity.runScript("window.setDgResult('$result')"); } } }
play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.auth.login import android.os.Build import android.os.Bundle import android.util.Base64 import android.util.Log import androidx.lifecycle.lifecycleScope import com.google.android.gms.fido.fido2.api.common.* import kotlinx.coroutines.CancellationException import org.json.JSONArray import org.json.JSONObject import org.microg.gms.fido.core.RequestHandlingException import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandlerCallback import org.microg.gms.fido.core.transport.bluetooth.BluetoothTransportHandler import org.microg.gms.fido.core.transport.nfc.NfcTransportHandler import org.microg.gms.fido.core.transport.screenlock.ScreenLockTransportHandler import org.microg.gms.fido.core.transport.usb.UsbTransportHandler import org.microg.gms.utils.toBase64 fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null fun JSONObject.getIntOrNull(key: String) = if (has(key)) getInt(key) else null fun JSONObject.getDoubleOrNull(key: String) = if (has(key)) getDouble(key) else null fun JSONObject.getArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallback { private lateinit var requestOptions: PublicKeyCredentialRequestOptions private val transportHandlers by lazy { setOfNotNull( BluetoothTransportHandler(activity, this), NfcTransportHandler(activity, this), if (Build.VERSION.SDK_INT >= 21) UsbTransportHandler(activity, this) else null, if (Build.VERSION.SDK_INT >= 23) ScreenLockTransportHandler(activity, this) else null ) } override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) { Log.d(TAG, "onStatusChanged: $transport, $status") } private fun sendEvent(type: String, data: JSONObject, extras: JSONObject? = null) { val event = JSONObject(extras?.toString() ?: "{}") event.put("type", type) event.put("data", data) activity.runScript("window.setFido2SkUiEvent($event)") } private fun sendResult(result: JSONObject) { activity.runScript("window.setFido2SkResult($result)") } private fun sendSelectView(viewName: String, extras: JSONObject? = null) { val data = JSONObject(extras?.toString() ?: "{}") data.put("viewName", viewName) sendEvent("select_view", data) } private fun sendErrorResult(errorCode: ErrorCode, errorMessage: String?) { Log.d(TAG, "Finish with error: $errorMessage ($errorCode)") sendResult(JSONObject().apply { put("errorCode", errorCode.code) if (errorMessage != null) put("errorMessage", errorMessage) }) } private fun sendSuccessResult(response: AuthenticatorResponse, transport: Transport) { Log.d(TAG, "Finish with success response: $response") if (response is AuthenticatorAssertionResponse) { sendResult(JSONObject().apply { val base64Flags = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE put("keyHandle", response.keyHandle?.toBase64(base64Flags)) put("clientDataJSON", response.clientDataJSON?.toBase64(base64Flags)) put("authenticatorData", response.authenticatorData?.toBase64(base64Flags)) put("signature", response.signature?.toBase64(base64Flags)) if (response.userHandle != null) { put("userHandle", response.userHandle?.toBase64(base64Flags)) } }) } } private val availableTransports: List<String> get() { val list = mutableListOf<String>() val transports = transportHandlers.filter { it.isSupported }.map { it.transport } if (Transport.BLUETOOTH in transports) { list.add("bt") list.add("ble") } if (Transport.USB in transports) list.add("usb") if (Transport.NFC in transports) list.add("nfc") if (Transport.SCREEN_LOCK in transports) list.add("internal") return list } fun startSignRequest(request: String) { try { val requestObject = JSONObject(request) requestOptions = PublicKeyCredentialRequestOptions.Builder().apply { val base64Flags = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE requestObject.getStringOrNull("challenge")?.let { setChallenge(Base64.decode(it, base64Flags)) } requestObject.getDoubleOrNull("timeoutSeconds")?.let { setTimeoutSeconds(it) } requestObject.getStringOrNull("rpId")?.let { setRpId(it) } requestObject.getArrayOrNull("allowList")?.let { val allowList = mutableListOf<PublicKeyCredentialDescriptor>() for (i in 0 until it.length()) { val obj = it.getJSONObject(i) allowList.add( PublicKeyCredentialDescriptor( obj.getStringOrNull("type") ?: "public-key", Base64.decode(obj.getString("id"), base64Flags), emptyList() ) ) } setAllowList(allowList) } requestObject.getIntOrNull("requestId")?.let { setRequestId(it) } }.build() Log.d(TAG, "sign: $requestOptions") sendSelectView("multiple_transports", JSONObject().apply { put("transports", JSONArray(availableTransports)) }) } catch (e: Exception) { Log.w(TAG, e) } } fun onEvent(event: String) { try { val eventObject = JSONObject(event) Log.d(TAG, "event: $eventObject") when (eventObject.getString("type")) { "user_selected_view_for_transport" -> { val transport = when (eventObject.getJSONObject("data").getString("transport")) { "bt" -> Transport.BLUETOOTH "ble" -> Transport.BLUETOOTH "nfc" -> Transport.NFC "usb" -> Transport.USB "internal" -> Transport.SCREEN_LOCK else -> return } val transportHandler = transportHandlers.firstOrNull { it.transport == transport && it.isSupported } ?: return activity.lifecycleScope.launchWhenStarted { val options = requestOptions try { sendSuccessResult(transportHandler.start(options, activity.packageName), transport) } catch (e: CancellationException) { Log.w(TAG, e) // Ignoring cancellation here } catch (e: RequestHandlingException) { Log.w(TAG, e) sendErrorResult(e.errorCode, e.message) } catch (e: Exception) { Log.w(TAG, e) sendErrorResult(ErrorCode.UNKNOWN_ERR, e.message) } } val extras = JSONObject().apply { put("alternateAvailableTransports", JSONArray(availableTransports)) } val viewName = when (transport) { Transport.NFC -> { extras.put("deviceRemovedTooSoon", false) extras.put("recommendUsb", false) "nfc_instructions" } Transport.USB -> "usb_instructions" Transport.BLUETOOTH -> "ble_instructions" else -> return } sendSelectView(viewName, extras) } } } catch (e: Exception) { Log.w(TAG, e) } } fun cancel() { Log.d(TAG, "cancel") } companion object { private const val TAG = "AuthFidoHandler" } }