Skip to content
Merged
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
Expand Up @@ -18,6 +18,7 @@ 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.hybrid.HybridTransportHandler
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
Expand All @@ -35,6 +36,7 @@ class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallbac
BluetoothTransportHandler(activity, this),
NfcTransportHandler(activity, this),
if (SDK_INT >= 21) UsbTransportHandler(activity, this) else null,
if (SDK_INT >= 21) HybridTransportHandler(activity, this) else null,
if (SDK_INT >= 23) ScreenLockTransportHandler(activity, this) else null
)
}
Expand Down Expand Up @@ -149,7 +151,7 @@ class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallbac
activity.lifecycleScope.launchWhenStarted {
val options = requestOptions
try {
sendSuccessResult(transportHandler.start(options, activity.packageName), transport)
sendSuccessResult(transportHandler.start(options, activity.packageName).response, transport)
} catch (e: CancellationException) {
Log.w(TAG, e)
// Ignoring cancellation here
Expand Down
3 changes: 3 additions & 0 deletions play-services-fido/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ dependencies {
implementation "com.android.volley:volley:$volleyVersion"
implementation 'com.upokecenter:cbor:4.5.2'
implementation 'com.google.guava:guava:31.1-android'

implementation 'com.google.zxing:core:3.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

android {
Expand Down
33 changes: 33 additions & 0 deletions play-services-fido/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
<uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"
tools:ignore="ProtectedPermissions" />

<!-- Bluetooth permissions for FIDO2 cross-device authentication -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- Bluetooth hardware features for FIDO2 -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />

<application>
<service
android:name=".privileged.Fido2PrivilegedService"
Expand Down Expand Up @@ -44,5 +58,24 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.Translucent"
android:name=".ui.hybrid.HybridAuthenticateActivity"
android:enabled="true"
android:exported="true"
android:process=":ui"
android:excludeFromRecents="true"
android:configChanges="smallestScreenSize|screenSize|uiMode|screenLayout|orientation|keyboardHidden|keyboard"
android:launchMode="singleTask"
android:noHistory="true"
tools:targetApi="23">
<intent-filter android:icon="@drawable/ic_fido_passkey">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="fido"/>
<data android:scheme="FIDO" tools:ignore="AppLinkUrlError" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE

fun getKnownRegistrationInfo(rpId: String) = readableDatabase.use {
val cursor = it.query(
TABLE_KNOWN_REGISTRATIONS, arrayOf(COLUMN_CREDENTIAL_ID, COLUMN_REGISTER_USER, COLUMN_TRANSPORT), "$COLUMN_RP_ID=?", arrayOf(rpId), null, null, null
TABLE_KNOWN_REGISTRATIONS,
arrayOf(COLUMN_CREDENTIAL_ID, COLUMN_REGISTER_USER, COLUMN_TRANSPORT),
"$COLUMN_RP_ID=?",
arrayOf(rpId),
null,
null,
"$COLUMN_TIMESTAMP DESC"
)
val result = mutableListOf<CredentialUserInfo>()
cursor.use { c ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,6 @@ val RequestOptions.rpId: String
SIGN -> signOptions.rpId
}

val RequestOptions.user: String?
get() = when (type) {
REGISTER -> registerOptions.user.toJson()
SIGN -> null
}

val PublicKeyCredentialCreationOptions.skipAttestation: Boolean
get() = attestationConveyancePreference in setOf(AttestationConveyancePreference.NONE, null)

Expand Down Expand Up @@ -108,7 +102,10 @@ private suspend fun isFacetIdTrusted(context: Context, facetIds: Set<String>, ap
return facetIds.any { trustedFacets.contains(it) }
}

private const val ASSET_LINK_REL = "delegate_permission/common.get_login_creds"
private val ASSET_LINK_REL = listOf(
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
)
private suspend fun isAssetLinked(context: Context, rpId: String, fp: String, packageName: String?): Boolean {
try {
val deferred = CompletableDeferred<JSONArray>()
Expand All @@ -118,7 +115,7 @@ private suspend fun isAssetLinked(context: Context, rpId: String, fp: String, pa
.add(JsonArrayRequest(url, { deferred.complete(it) }, { deferred.completeExceptionally(it) }))
val arr = deferred.await()
for (obj in arr.map(JSONArray::getJSONObject)) {
if (!obj.getJSONArray("relation").map(JSONArray::getString).contains(ASSET_LINK_REL)) continue
if (obj.getJSONArray("relation").map(JSONArray::getString).none { ASSET_LINK_REL.contains(it) }) continue
val target = obj.getJSONObject("target")
if (target.getString("namespace") != "android_app") continue
if (packageName != null && target.getString("package_name") != packageName) continue
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.fido.core.hybrid.ble

import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import org.microg.gms.fido.core.hybrid.UUID_ANDROID
import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean

private const val TAG = "HybridBleAdvertiser"
private const val ADVERTISE_TIMEOUT_MS = 20000L

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class HybridBleAdvertiser(
private val bluetoothLeAdapter: BluetoothAdapter?,
private val onAdvertiseFailure: ((Int) -> Unit)? = null
) : AdvertiseCallback() {
private val advertiserStatus = AtomicBoolean(false)
private val handler = Handler(Looper.getMainLooper())
private val stopRunnable = object : Runnable {
@RequiresPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
override fun run() {
stopAdvertising()
}
}

private val bluetoothLeAdvertiser by lazy {
if (bluetoothLeAdapter != null) {
bluetoothLeAdapter.bluetoothLeAdvertiser
} else {
Log.d(TAG, "BLE_HARDWARE ERROR")
null
}
}

@RequiresPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
fun startAdvertising(eid: ByteArray) {
if (advertiserStatus.compareAndSet(false, true)) {
val settings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(false)
.setTimeout(ADVERTISE_TIMEOUT_MS.toInt())
.build()

val data = AdvertiseData.Builder()
.addServiceUuid(UUID_ANDROID)
.addServiceData(UUID_ANDROID, eid)
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
.build()

bluetoothLeAdvertiser?.startAdvertising(settings, data, this)

handler.postDelayed(stopRunnable, ADVERTISE_TIMEOUT_MS)
}
}

@RequiresPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
fun stopAdvertising() {
if (this.advertiserStatus.compareAndSet(true, false)) {
handler.removeCallbacks(stopRunnable)
Log.d(TAG, "BLE_ADVERTISING_STOP")
bluetoothLeAdvertiser?.stopAdvertising(this)
}
}

override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
super.onStartSuccess(settingsInEffect)
Log.d(TAG, String.format("BLE advertising onStartSuccess: %s", settingsInEffect))
}

override fun onStartFailure(errorCode: Int) {
super.onStartFailure(errorCode)
Log.d(TAG, String.format("BLE advertising onStartFailure: %d", errorCode))
advertiserStatus.set(false)
handler.removeCallbacks(stopRunnable)
onAdvertiseFailure?.invoke(errorCode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.fido.core.hybrid.ble

import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import com.google.android.gms.fido.fido2.api.common.ErrorCode
import org.microg.gms.fido.core.RequestHandlingException
import org.microg.gms.fido.core.hybrid.EMPTY_SERVICE_DATA
import org.microg.gms.fido.core.hybrid.EMPTY_SERVICE_DATA_MASK
import org.microg.gms.fido.core.hybrid.UUID_ANDROID
import org.microg.gms.fido.core.hybrid.UUID_IOS
import org.microg.gms.fido.core.hybrid.utils.CryptoHelper
import java.util.concurrent.atomic.AtomicBoolean

private const val TAG = "HybridClientScan"

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class HybridClientScan(
private val bluetoothLeAdapter: BluetoothAdapter?,
private val seed: ByteArray,
private val onScanSuccess: (ByteArray) -> Unit,
private val onScanFailed: (Throwable) -> Unit
) : ScanCallback() {

private val scanStatus = AtomicBoolean(false)

@RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
override fun onScanResult(callbackType: Int, result: ScanResult) {
val scanRecord = result.scanRecord
if (scanRecord == null) {
Log.d(TAG, "processDevice: ScanResult is missing ScanRecord")
return
}
var serviceData = scanRecord.getServiceData(UUID_ANDROID)
if (serviceData == null) {
serviceData = scanRecord.getServiceData(UUID_IOS)
}
if (serviceData == null || serviceData.size != 20) {
Log.d(TAG, "processDevice: Invalid service data, skipping")
return
}
Log.d(TAG, "Target device with EID: ${serviceData.joinToString("") { "%02x".format(it) }}")
if (CryptoHelper.decryptEid(serviceData, seed) == null) {
Log.d(TAG, "EID verification failed, not matching current session, continuing scan")
return
}
stopScanning()
onScanSuccess(serviceData)
}

override fun onScanFailed(errorCode: Int) {
onScanFailed(RequestHandlingException(ErrorCode.UNKNOWN_ERR, "BLE scan failed: $errorCode"))
}

@SuppressLint("MissingPermission")
fun startScanning() {
if (scanStatus.compareAndSet(false, true)) {
try {
val adapter = bluetoothLeAdapter ?: throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR, "BluetoothAdapter null")
if (!adapter.isEnabled) {
val enabled = adapter.enable()
if (!enabled) {
throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR, "Unable to enable Bluetooth")
}
}
val scanner = adapter.bluetoothLeScanner ?: throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR, "BluetoothLeScanner null")

val filters = listOf(
ScanFilter.Builder().setServiceData(UUID_ANDROID, EMPTY_SERVICE_DATA, EMPTY_SERVICE_DATA_MASK).build(),
ScanFilter.Builder().setServiceData(UUID_IOS, EMPTY_SERVICE_DATA, EMPTY_SERVICE_DATA_MASK).build()
)
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
scanner.startScan(filters, settings, this)
Log.d(TAG, "BLE scanning started")
} catch (t: Throwable) {
onScanFailed(RequestHandlingException(ErrorCode.UNKNOWN_ERR, "startScan failed: ${t.message}"))
}
}
}

@RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
fun stopScanning() {
if (scanStatus.compareAndSet(true, false)) {
try {
val bluetoothLeScanner = bluetoothLeAdapter?.bluetoothLeScanner
bluetoothLeScanner?.stopScan(this)
Log.d(TAG, "BLE scanning stopped")
} catch (t: Throwable) {
onScanFailed(RequestHandlingException(ErrorCode.UNKNOWN_ERR, "stopScan failed: ${t.message}"))
}
}
}
}
Loading
Loading