Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit ff5c5a7e authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Fido: Add support for fido authentication during Google Account sign-in

parent 0830d301
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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">
+2 −1
Original line number Diff line number Diff line
@@ -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);

+98 −5
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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));
@@ -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");
@@ -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
@@ -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();
@@ -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) {
@@ -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);
@@ -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);
        }

    }
}
+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')");
        }
    }
}
+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