Skip to content
Open
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
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
39 changes: 39 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,30 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.Translucent"
android:name=".ui.QRBounceActivity"
android:exported="true"
android:process=":ui"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:noHistory="true">
<intent-filter>
<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>
<activity
android:theme="@style/Theme.Translucent"
android:name=".ui.hybrid.HybridAuthenticateActivity"
android:enabled="true"
android:exported="false"
android:process=":ui"
android:excludeFromRecents="true"
android:configChanges="smallestScreenSize|screenSize|uiMode|screenLayout|orientation|keyboardHidden|keyboard"
tools:targetApi="23" />
</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 @@ -108,7 +108,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 +121,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,87 @@
/**
* 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 java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean

private const val TAG = "HybridBleAdvertiser"

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class HybridBleAdvertiser(
private val bluetoothLeAdapter: BluetoothAdapter?,
) : AdvertiseCallback() {
private val advertiserStatus = AtomicBoolean(false)
private val timer = Timer()
private val stopTimeTask = object : TimerTask() {
@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(0)
.build()

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

bluetoothLeAdvertiser?.startAdvertising(settings, data, this)

timer.schedule(stopTimeTask, 10000L)
}
}

@RequiresPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
fun stopAdvertising() {
if (this.advertiserStatus.compareAndSet(true, false)) {
timer.cancel()
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))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* 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 java.util.concurrent.atomic.AtomicBoolean

private const val TAG = "HybridClientScan"

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class HybridClientScan(
private val bluetoothLeAdapter: BluetoothAdapter?, 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) {
stopScanning()
val scanRecord = result.scanRecord
if (scanRecord == null) {
Log.d(TAG, "processDevice: ScanResult is missing ScanRecord")
return onScanFailed(RequestHandlingException(ErrorCode.DATA_ERR, "ScanResult is missing ScanRecord."))
}
var serviceData = scanRecord.getServiceData(UUID_ANDROID)
if (serviceData == null) {
Log.d(TAG, "processDevice: No service data, checking iOS UUID")
serviceData = scanRecord.getServiceData(UUID_IOS)
}
if (serviceData == null) {
Log.d(TAG, "processDevice: ScanRecord does not contain service data.")
return onScanFailed(RequestHandlingException(ErrorCode.DATA_ERR, "ScanRecord does not contain service data."))
}
if (serviceData.size != 20) {
Log.d(TAG, "processDevice: Service data is incorrect size.")
return onScanFailed(RequestHandlingException(ErrorCode.DATA_ERR, "Service data is incorrect size."))
}
Log.d(TAG, "Target device with EID: ${serviceData.joinToString("") { "%02x".format(it) }}")
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