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,43 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
namespace "com.google.android.gms.fido"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
buildFeatures {
aidl = true
}
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG implementation of play-services-fido'
dependencies {
// Dependencies from play-services-fido:18.1.0
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-tasks')
annotationProcessor project(':safe-parcel-processor')
}

View file

@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'
apply plugin: 'signing'
dependencies {
api project(':play-services-fido')
implementation project(':play-services-base-core')
implementation project(':play-services-safetynet')
implementation project(':play-services-tasks-ktx')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
implementation "com.android.volley:volley:$volleyVersion"
implementation 'com.upokecenter:cbor:4.5.2'
implementation 'com.google.guava:guava:31.1-android'
}
android {
namespace "org.microg.gms.fido.core"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
buildFeatures {
dataBinding = true
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'MissingTranslation', 'GetLocales'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}
apply from: '../../gradle/publish-android.gradle'
description = 'microG service implementation for play-services-fido'

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission
android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"
tools:ignore="ProtectedPermissions" />
<application>
<service
android:name=".privileged.Fido2PrivilegedService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.fido.fido2.privileged.START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name=".regular.Fido2AppService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.fido.fido2.regular.START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<activity
android:name=".ui.AuthenticatorActivity"
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
android:exported="false"
android:process=":ui"
android:theme="@style/Theme.Translucent">
<intent-filter>
<action android:name="org.microg.gms.fido.AUTHENTICATE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.ui.TAG
class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VERSION) {
fun isPrivileged(packageName: String, signatureDigest: String): Boolean = readableDatabase.use {
it.count(TABLE_PRIVILEGED_APPS, "$COLUMN_PACKAGE_NAME = ? AND $COLUMN_SIGNATURE_DIGEST = ?", packageName, signatureDigest) > 0
}
fun wasUsed(): Boolean = readableDatabase.use { it.count(TABLE_KNOWN_REGISTRATIONS) > 0 }
fun getKnownRegistrationTransport(rpId: String, credentialId: String) = readableDatabase.use {
val c = it.query(TABLE_KNOWN_REGISTRATIONS, arrayOf(COLUMN_TRANSPORT), "$COLUMN_RP_ID = ? AND $COLUMN_CREDENTIAL_ID = ?", arrayOf(rpId, credentialId), null, null, null)
try {
if (c.moveToFirst()) Transport.valueOf(c.getString(0)) else null
} finally {
c.close()
}
}
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
)
val result = mutableListOf<CredentialUserInfo>()
cursor.use { c ->
while (c.moveToNext()) {
val credentialId = c.getString(0)
val userJson = c.getStringOrNull(1) ?: continue
val transport = c.getStringOrNull(2) ?: continue
Log.d(TAG, "getKnownRegistrationInfo: credential: $credentialId user: $userJson transport: $transport")
result.add(CredentialUserInfo(credentialId, userJson, Transport.valueOf(transport)))
}
}
result
}
fun insertPrivileged(packageName: String, signatureDigest: String) = writableDatabase.use {
it.insertWithOnConflict(TABLE_PRIVILEGED_APPS, null, ContentValues().apply {
put(COLUMN_PACKAGE_NAME, packageName)
put(COLUMN_SIGNATURE_DIGEST, signatureDigest)
put(COLUMN_TIMESTAMP, System.currentTimeMillis())
}, CONFLICT_REPLACE)
}
fun insertKnownRegistration(rpId: String, credentialId: String, transport: Transport, userJson: String? = null) = writableDatabase.use {
Log.d(TAG, "insertKnownRegistration: $rpId $credentialId $transport $userJson")
val values = ContentValues().apply {
put(COLUMN_CREDENTIAL_ID, credentialId)
put(COLUMN_TRANSPORT, transport.name)
put(COLUMN_TIMESTAMP, System.currentTimeMillis())
if (userJson != null) {
put(COLUMN_REGISTER_USER, userJson)
}
}
val updated = if (userJson == null) {
it.update(TABLE_KNOWN_REGISTRATIONS, values, "$COLUMN_RP_ID = ? AND $COLUMN_CREDENTIAL_ID = ?", arrayOf(rpId, credentialId))
} else {
it.update(TABLE_KNOWN_REGISTRATIONS, values, "$COLUMN_RP_ID = ? AND $COLUMN_REGISTER_USER = ?", arrayOf(rpId, userJson))
}
if (updated == 0) {
val insertValues = ContentValues().apply {
put(COLUMN_RP_ID, rpId)
put(COLUMN_CREDENTIAL_ID, credentialId)
put(COLUMN_TRANSPORT, transport.name)
put(COLUMN_TIMESTAMP, System.currentTimeMillis())
userJson?.let { json -> put(COLUMN_REGISTER_USER, json) }
}
it.insert(TABLE_KNOWN_REGISTRATIONS, null, insertValues)
}
}
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, VERSION)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 1) {
db.execSQL("CREATE TABLE $TABLE_PRIVILEGED_APPS($COLUMN_PACKAGE_NAME TEXT, $COLUMN_SIGNATURE_DIGEST TEXT, $COLUMN_TIMESTAMP INT, UNIQUE($COLUMN_PACKAGE_NAME, $COLUMN_SIGNATURE_DIGEST) ON CONFLICT REPLACE);")
}
if (oldVersion < 2) {
db.execSQL("CREATE TABLE $TABLE_KNOWN_REGISTRATIONS($COLUMN_RP_ID TEXT, $COLUMN_CREDENTIAL_ID TEXT, $COLUMN_TRANSPORT TEXT, $COLUMN_TIMESTAMP INT, UNIQUE($COLUMN_RP_ID, $COLUMN_CREDENTIAL_ID) ON CONFLICT REPLACE)")
}
if (oldVersion < 3) {
db.execSQL("ALTER TABLE $TABLE_KNOWN_REGISTRATIONS ADD COLUMN $COLUMN_REGISTER_USER TEXT")
}
}
companion object {
const val VERSION = 3
private const val TABLE_PRIVILEGED_APPS = "privileged_apps"
private const val TABLE_KNOWN_REGISTRATIONS = "known_registrations"
private const val COLUMN_PACKAGE_NAME = "package_name"
private const val COLUMN_SIGNATURE_DIGEST = "signature_digest"
private const val COLUMN_TIMESTAMP = "timestamp"
private const val COLUMN_RP_ID = "rp_id"
private const val COLUMN_CREDENTIAL_ID = "credential_id"
private const val COLUMN_TRANSPORT = "transport"
private const val COLUMN_REGISTER_USER = "register_user"
}
}
fun SQLiteDatabase.count(table: String, selection: String? = null, vararg selectionArgs: String): Long {
val it = if (selection == null) {
rawQuery("SELECT COUNT(*) FROM $table", null)
} else {
rawQuery("SELECT COUNT(*) FROM $table WHERE $selection", selectionArgs)
}
return try {
if (it.moveToFirst()) {
it.getLongOrNull(0) ?: 0
} else {
0
}
} finally {
it.close()
}
}

View file

@ -0,0 +1,260 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core
import android.content.Context
import android.net.Uri
import android.util.Base64
import android.util.Log
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.ErrorCode.*
import com.google.common.net.InternetDomainName
import kotlinx.coroutines.CompletableDeferred
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.fido.core.RequestOptionsType.REGISTER
import org.microg.gms.fido.core.RequestOptionsType.SIGN
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.utils.*
import java.net.HttpURLConnection
import java.security.MessageDigest
private const val TAG = "Fido"
class RequestHandlingException(val errorCode: ErrorCode, message: String? = null) : Exception(message)
class MissingPinException(message: String? = null): Exception(message)
class WrongPinException(message: String? = null): Exception(message)
data class CredentialUserInfo(val credential: String, val userJson: String, val transport: Transport)
enum class RequestOptionsType { REGISTER, SIGN }
val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions
get() = when (this) {
is BrowserPublicKeyCredentialCreationOptions -> publicKeyCredentialCreationOptions
is PublicKeyCredentialCreationOptions -> this
else -> throw RequestHandlingException(DATA_ERR, "The request options are not valid")
}
val RequestOptions.signOptions: PublicKeyCredentialRequestOptions
get() = when (this) {
is BrowserPublicKeyCredentialRequestOptions -> publicKeyCredentialRequestOptions
is PublicKeyCredentialRequestOptions -> this
else -> throw RequestHandlingException(DATA_ERR, "The request options are not valid")
}
val RequestOptions.type: RequestOptionsType
get() = when (this) {
is PublicKeyCredentialCreationOptions, is BrowserPublicKeyCredentialCreationOptions -> REGISTER
is PublicKeyCredentialRequestOptions, is BrowserPublicKeyCredentialRequestOptions -> SIGN
else -> throw RequestHandlingException(INVALID_STATE_ERR)
}
val RequestOptions.webAuthnType: String
get() = when (type) {
REGISTER -> "webauthn.create"
SIGN -> "webauthn.get"
}
val RequestOptions.challenge: ByteArray
get() = when (type) {
REGISTER -> registerOptions.challenge
SIGN -> signOptions.challenge
}
val RequestOptions.rpId: String
get() = when (type) {
REGISTER -> registerOptions.rp.id
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)
fun topDomainOf(string: String?) =
string?.let { InternetDomainName.from(string).topDomainUnderRegistrySuffix().toString() }
fun <T> JSONArray.map(fn: JSONArray.(Int) -> T): List<T> = (0 until length()).map { fn(this, it) }
private suspend fun isFacetIdTrusted(context: Context, facetIds: Set<String>, appId: String): Boolean {
val trustedFacets = try {
val deferred = CompletableDeferred<JSONObject>()
HttpURLConnection.setFollowRedirects(false)
singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
.add(JsonObjectRequest(appId, { deferred.complete(it) }, { deferred.completeExceptionally(it) }))
val obj = deferred.await()
val arr = obj.getJSONArray("trustedFacets")
if (arr.length() > 1) {
// Unsupported
emptyList()
} else {
arr.getJSONObject(0).getJSONArray("ids").map(JSONArray::getString)
}
} catch (e: Exception) {
// Ignore and fail
emptyList()
}
return facetIds.any { trustedFacets.contains(it) }
}
private const val ASSET_LINK_REL = "delegate_permission/common.get_login_creds"
private suspend fun isAssetLinked(context: Context, rpId: String, fp: String, packageName: String?): Boolean {
try {
val deferred = CompletableDeferred<JSONArray>()
HttpURLConnection.setFollowRedirects(true)
val url = "https://$rpId/.well-known/assetlinks.json"
singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
.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
val target = obj.getJSONObject("target")
if (target.getString("namespace") != "android_app") continue
if (packageName != null && target.getString("package_name") != packageName) continue
for (fingerprint in target.getJSONArray("sha256_cert_fingerprints").map(JSONArray::getString)) {
if (fingerprint.equals(fp, ignoreCase = true)) return true
}
}
Log.w(TAG, "No matching asset link")
return false
} catch (e: Exception) {
Log.w(TAG, "Failed fetching asset link", e)
return false
}
}
// Note: This assumes the RP ID is allowed
private suspend fun isAppIdAllowed(context: Context, appId: String, facetIds: Set<String>, rpId: String): Boolean {
return try {
when {
topDomainOf(Uri.parse(appId).host) == topDomainOf(rpId) -> {
// Valid: AppId TLD+1 matches RP ID
true
}
topDomainOf(Uri.parse(appId).host) == "gstatic.com" && rpId == "google.com" -> {
// Valid: Hardcoded support for Google putting their app id under gstatic.com.
// This is gonna save us a ton of requests
true
}
isFacetIdTrusted(context, facetIds, appId) -> {
// Valid: Allowed by TrustedFacets list
true
}
else -> {
false
}
}
} catch (e: Exception) {
false
}
}
suspend fun RequestOptions.checkIsValid(context: Context, origin: String, packageName: String?) {
val allApplicableFacetIds = hashSetOf<String>()
if (origin.startsWith("https://")) {
allApplicableFacetIds.add(origin)
if (topDomainOf(Uri.parse(origin).host) != topDomainOf(rpId)) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin")
}
// FIXME: Standard suggests doing additional checks, but this is already sensible enough
} else if ((origin.startsWith("android:apk-key-hash:") || origin.startsWith("android:apk-key-hash-sha256:")) && packageName != null) {
allApplicableFacetIds.addAll(getAllFacetIdCandidates(context, packageName, origin))
val sha256facetId = allApplicableFacetIds.firstOrNull { it.startsWith("android:apk-key-hash-sha256:") }
?: throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin")
val fp = Base64.decode(sha256facetId.substring(28), HASH_BASE64_FLAGS).toHexString(":")
if (!isAssetLinked(context, rpId, fp, packageName)) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin (expected fingerprint $fp)")
}
} else {
throw RequestHandlingException(NOT_SUPPORTED_ERR, "Origin $origin not supported")
}
val appId = authenticationExtensions?.fidoAppIdExtension?.appId
if (appId != null) {
if (!appId.startsWith("https://")) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId must start with https://")
}
if (Uri.parse(appId).host.isNullOrEmpty()) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId must have a valid hostname")
}
if (!isAppIdAllowed(context, appId, allApplicableFacetIds, rpId)) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId not allowed from facets [${allApplicableFacetIds.joinToString()}]")
}
}
}
private const val HASH_BASE64_FLAGS = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE
fun RequestOptions.getWebAuthnClientData(callingPackage: String, origin: String): ByteArray {
val obj = JSONObject()
.put("type", webAuthnType)
.put("challenge", challenge.toBase64(HASH_BASE64_FLAGS))
.put("androidPackageName", callingPackage)
.put("tokenBinding", tokenBinding?.toJsonObject())
.put("origin", origin)
return obj.toString().encodeToByteArray()
}
fun getApplicationName(context: Context, options: RequestOptions, callingPackage: String): String = when (options) {
is BrowserPublicKeyCredentialCreationOptions, is BrowserPublicKeyCredentialRequestOptions -> options.rpId
else -> context.packageManager.getApplicationLabel(callingPackage).toString()
}
fun getApkKeyHashOrigin(context: Context, packageName: String): String {
val digest = context.packageManager.getFirstSignatureDigest(packageName, "SHA-256")
?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName")
return "android:apk-key-hash:${digest.toBase64(HASH_BASE64_FLAGS)}"
}
fun getAllFacetIdCandidates(context: Context, packageName: String, origin: String): List<String> {
val firstSignature = context.packageManager.getSignatures(packageName).firstOrNull()
?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName")
val sha1 = firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)
val sha256 = firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)
val candidates = arrayListOf(
"android:apk-key-hash:$sha1",
"android:apk-key-hash:$sha256",
"android:apk-key-hash-sha256:$sha256",
)
if (!candidates.contains(origin))
throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName ($origin)")
return candidates
}
fun getOrigin(context: Context, options: RequestOptions, callingPackage: String): String = when {
options is BrowserRequestOptions -> {
if (options.origin.scheme == null || options.origin.authority == null) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "Bad url ${options.origin}")
}
"${options.origin.scheme}://${options.origin.authority}"
}
else -> getApkKeyHashOrigin(context, callingPackage)
}
fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this)
fun getClientDataAndHash(
context: Context,
options: RequestOptions,
callingPackage: String
): Pair<ByteArray, ByteArray> {
val clientData: ByteArray?
var clientDataHash = (options as? BrowserRequestOptions)?.clientDataHash
if (clientDataHash == null) {
clientData = options.getWebAuthnClientData(callingPackage, getOrigin(context, options, callingPackage))
clientDataHash = clientData.digest("SHA-256")
} else {
clientData = "<invalid>".toByteArray()
}
return clientData to clientDataHash
}

View file

@ -0,0 +1,21 @@
package org.microg.gms.fido.core
import com.google.android.gms.common.Feature
val FEATURES = arrayOf(
Feature("cancel_target_direct_transfer", 1),
Feature("delete_credential", 1),
Feature("delete_device_public_key", 1),
Feature("get_or_generate_device_public_key", 1),
Feature("get_passkeys", 1),
Feature("update_passkey", 1),
Feature("is_user_verifying_platform_authenticator_available_for_credential", 1),
Feature("is_user_verifying_platform_authenticator_available", 1),
Feature("privileged_api_list_credentials", 2),
Feature("start_target_direct_transfer", 1),
Feature("first_party_api_get_link_info", 1),
Feature("get_browser_hybrid_client_sign_pending_intent", 1),
Feature("get_browser_hybrid_client_registration_pending_intent", 1),
Feature("privileged_authenticate_passkey", 1),
Feature("privileged_register_passkey_with_sync_account", 1)
)

View file

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.privileged
import android.app.KeyguardManager
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Context.KEYGUARD_SERVICE
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Parcel
import android.util.Log
import androidx.core.app.PendingIntentCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
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 com.google.android.gms.fido.fido2.api.IBooleanCallback
import com.google.android.gms.fido.fido2.api.ICredentialListCallback
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialCreationOptions
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialRequestOptions
import com.google.android.gms.fido.fido2.internal.privileged.IFido2PrivilegedCallbacks
import com.google.android.gms.fido.fido2.internal.privileged.IFido2PrivilegedService
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.common.GmsService.FIDO2_PRIVILEGED
import org.microg.gms.fido.core.FEATURES
import org.microg.gms.fido.core.ui.AuthenticatorActivity
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.SOURCE_BROWSER
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SOURCE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_OPTIONS
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SERVICE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_TYPE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_REGISTER
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_SIGN
import org.microg.gms.utils.warnOnTransactionIssues
const val TAG = "Fido2Privileged"
class Fido2PrivilegedService : BaseService(TAG, FIDO2_PRIVILEGED) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS,
Fido2PrivilegedServiceImpl(this, lifecycle).asBinder(),
ConnectionInfo().apply { features = FEATURES }
);
}
}
class Fido2PrivilegedServiceImpl(private val context: Context, override val lifecycle: Lifecycle) :
IFido2PrivilegedService.Stub(), LifecycleOwner {
override fun getRegisterPendingIntent(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialCreationOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_PRIVILEGED.SERVICE_ID)
.putExtra(KEY_SOURCE, SOURCE_BROWSER)
.putExtra(KEY_TYPE, TYPE_REGISTER)
.putExtra(KEY_OPTIONS, options.serializeToBytes())
val pendingIntent =
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
}
}
override fun getSignPendingIntent(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialRequestOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_PRIVILEGED.SERVICE_ID)
.putExtra(KEY_SOURCE, SOURCE_BROWSER)
.putExtra(KEY_TYPE, TYPE_SIGN)
.putExtra(KEY_OPTIONS, options.serializeToBytes())
val pendingIntent =
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
}
}
override fun isUserVerifyingPlatformAuthenticatorAvailable(callbacks: IBooleanCallback) {
lifecycleScope.launchWhenStarted {
if (SDK_INT < 24) {
callbacks.onBoolean(false)
} else {
val keyguardManager = context.getSystemService(KEYGUARD_SERVICE) as? KeyguardManager?
callbacks.onBoolean(keyguardManager?.isDeviceSecure == true)
}
}
}
override fun getCredentialList(callbacks: ICredentialListCallback, rpId: String) {
Log.w(TAG, "Not yet implemented: getCredentialList")
lifecycleScope.launchWhenStarted {
runCatching { callbacks.onCredentialList(emptyList()) }
}
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) }
}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.google.android.gms.fido.fido2.api.common.Algorithm
import com.upokecenter.cbor.CBORObject
class AndroidKeyAttestationObject(
authData: AuthenticatorData,
val alg: Algorithm,
val sig: ByteArray,
val x5c: List<ByteArray>
) :
AttestationObject(authData.encode()) {
override val fmt: String
get() = "android-key"
override val attStmt: CBORObject
get() = CBORObject.NewMap().apply {
set("alg", alg.algoValue.encodeAsCbor())
set("sig", sig.encodeAsCbor())
set("x5c", CBORObject.NewArray().apply {
for (certificate in x5c) {
Add(certificate.encodeAsCbor())
}
})
}
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.upokecenter.cbor.CBORObject
class AndroidSafetyNetAttestationObject(authData: AuthenticatorData, val ver: String, val response: ByteArray) :
AttestationObject(authData.encode()) {
override val fmt: String
get() = "android-safetynet"
override val attStmt: CBORObject
get() = CBORObject.NewMap().apply {
set("ver", ver.encodeAsCbor())
set("response", response.encodeAsCbor())
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.upokecenter.cbor.CBOREncodeOptions
import com.upokecenter.cbor.CBORObject
abstract class AttestationObject(val authData: ByteArray) {
abstract val fmt: String
abstract val attStmt: CBORObject
fun encode(): ByteArray = CBORObject.NewMap().apply {
set("fmt", fmt.encodeAsCbor())
set("attStmt", attStmt)
set("authData", authData.encodeAsCbor())
}.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical)
}
class AnyAttestationObject(authData: ByteArray, override val fmt: String, override val attStmt: CBORObject) : AttestationObject(authData)

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.upokecenter.cbor.CBORObject
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class AttestedCredentialData(val aaguid: ByteArray, val id: ByteArray, val publicKey: ByteArray) {
fun encode() = ByteBuffer.allocate(aaguid.size + 2 + id.size + publicKey.size)
.put(aaguid)
.order(ByteOrder.BIG_ENDIAN).putShort(id.size.toShort())
.put(id)
.put(publicKey)
.array()
companion object {
fun decode(buffer: ByteBuffer) = buffer.run {
val aaguid = ByteArray(16)
get(aaguid)
val idSize = order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
val id = ByteArray(idSize)
get(id)
mark()
val remaining = ByteArray(remaining())
get(remaining)
val bis = ByteArrayInputStream(remaining)
CBORObject.Read(bis) // Read object and ignore, we only want to know the size
reset()
val publicKey = ByteArray(remaining() - bis.available())
get(publicKey)
return@run AttestedCredentialData(aaguid, id, publicKey)
}
}
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.experimental.and
import kotlin.experimental.or
class AuthenticatorData(
val rpIdHash: ByteArray,
val userPresent: Boolean,
val userVerified: Boolean,
val signCount: Int,
val attestedCredentialData: AttestedCredentialData? = null,
val extensions: ByteArray? = null
) {
fun encode(): ByteArray {
val attestedCredentialData = attestedCredentialData?.encode() ?: ByteArray(0)
val extensions = extensions ?: ByteArray(0)
return ByteBuffer.allocate(rpIdHash.size + 5 + attestedCredentialData.size + extensions.size)
.put(rpIdHash)
.put(buildFlags(userPresent, userVerified, attestedCredentialData.isNotEmpty(), extensions.isNotEmpty()))
.order(ByteOrder.BIG_ENDIAN).putInt(signCount)
.put(attestedCredentialData)
.put(extensions)
.array()
}
companion object {
/** User Present **/
private const val FLAG_UP: Byte = 0x01
/** User Verified **/
private const val FLAG_UV: Byte = 0x04
/** Attested credential data included **/
private const val FLAG_AT: Byte = 0x40
/** Extension data included **/
private const val FLAG_ED: Byte = -0x80
private fun buildFlags(up: Boolean, uv: Boolean, at: Boolean, ed: Boolean): Byte =
(if (up) FLAG_UP else 0) or (if (uv) FLAG_UV else 0) or (if (at) FLAG_AT else 0) or (if (ed) FLAG_ED else 0)
fun decode(byteArray: ByteArray): AuthenticatorData = ByteBuffer.wrap(byteArray).run {
val rpIdHash = ByteArray(32)
get(rpIdHash)
val flags = get()
val signCount = order(ByteOrder.BIG_ENDIAN).int
val attestedCredentialData = if ((flags and FLAG_AT) == FLAG_AT) AttestedCredentialData.decode(this) else null
val extensions = if ((flags and FLAG_ED) == FLAG_ED) {
val ed = ByteArray(remaining())
get(ed)
ed
} else {
null
}
return@run AuthenticatorData(rpIdHash, flags and FLAG_UP == FLAG_UP, flags and FLAG_UV == FLAG_UV, signCount, attestedCredentialData, extensions)
}
}
}

View file

@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import android.util.Log
import com.google.android.gms.fido.common.Transport
import com.google.android.gms.fido.fido2.api.common.Algorithm
import com.google.android.gms.fido.fido2.api.common.EC2Algorithm
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
import com.google.android.gms.fido.fido2.api.common.RSAAlgorithm
import com.upokecenter.cbor.CBORObject
private const val TAG = "FidoCbor"
fun CBORObject.AsStringSequence(): Iterable<String> = Iterable {
object : Iterator<String> {
var index = 0
override fun hasNext(): Boolean = size() + 1 < index
override fun next(): String = get(index++).AsString()
}
}
fun CBORObject.AsInt32Sequence(): Iterable<Int> = Iterable {
object : Iterator<Int> {
var index = 0
override fun hasNext(): Boolean = size() + 1 < index
override fun next(): Int = get(index++).AsInt32()
}
}
fun String.encodeAsCbor() = CBORObject.FromObject(this)
fun ByteArray.encodeAsCbor() = CBORObject.FromObject(this)
fun Int.encodeAsCbor() = CBORObject.FromObject(this)
fun Boolean.encodeAsCbor() = CBORObject.FromObject(this)
fun PublicKeyCredentialRpEntity.encodeAsCbor() = CBORObject.NewMap().apply {
set("id", id.encodeAsCbor())
if (!name.isNullOrBlank()) set("name", name.encodeAsCbor())
if (!icon.isNullOrBlank()) set("icon", icon!!.encodeAsCbor())
}
fun PublicKeyCredentialUserEntity.encodeAsCbor() = CBORObject.NewMap().apply {
set("id", id.encodeAsCbor())
if (!name.isNullOrBlank()) set("name", name.encodeAsCbor())
if (!icon.isNullOrBlank()) set("icon", icon!!.encodeAsCbor())
if (!displayName.isNullOrBlank()) set("displayName", displayName.encodeAsCbor())
}
fun CBORObject.decodeAsPublicKeyCredentialUserEntity() = PublicKeyCredentialUserEntity(
get("id")?.GetByteString() ?: ByteArray(0).also { Log.w(TAG, "id was not present") },
get("name")?.AsString() ?: "".also { Log.w(TAG, "name was not present") },
get("icon")?.AsString(),
get("displayName")?.AsString() ?: "".also { Log.w(TAG, "displayName was not present") }
)
fun CBORObject.decodeAsCoseKey() = CoseKey(
getAlgorithm(get(CoseKey.ALG).AsInt32Value()),
get(CoseKey.X).GetByteString(),
get(CoseKey.Y).GetByteString(),
get(CoseKey.CRV).AsInt32Value()
)
fun getAlgorithm(algorithmInt: Int): Algorithm {
return when (algorithmInt) {
-65535 -> RSAAlgorithm.RS1
-262 -> RSAAlgorithm.LEGACY_RS1
-261 -> EC2Algorithm.ED512
-260 -> EC2Algorithm.ED256
-259 -> RSAAlgorithm.RS512
-258 -> RSAAlgorithm.RS384
-257 -> RSAAlgorithm.RS256
-39 -> RSAAlgorithm.PS512
-38 -> RSAAlgorithm.PS384
-37 -> RSAAlgorithm.PS256
-36 -> EC2Algorithm.ES512
-35 -> EC2Algorithm.ES384
-7 -> EC2Algorithm.ES256
else -> Algorithm { algorithmInt }
}
}
fun PublicKeyCredentialParameters.encodeAsCbor() = CBORObject.NewMap().apply {
set("alg", algorithmIdAsInteger.encodeAsCbor())
set("type", typeAsString.encodeAsCbor())
}
fun CBORObject.decodeAsPublicKeyCredentialParameters() = PublicKeyCredentialParameters(
get("type").AsString(),
get("alg").AsInt32Value()
)
fun PublicKeyCredentialDescriptor.encodeAsCbor() = CBORObject.NewMap().apply {
set("type", typeAsString.encodeAsCbor())
set("id", id.encodeAsCbor())
set("transports", transports.orEmpty().encodeAsCbor { it.toString().encodeAsCbor() })
}
fun CBORObject.decodeAsPublicKeyCredentialDescriptor() = PublicKeyCredentialDescriptor(
get("type")?.AsString() ?: "".also { Log.w(TAG, "type was not present") },
get("id")?.GetByteString() ?: ByteArray(0).also { Log.w(TAG, "id was not present") },
get("transports")?.AsStringSequence()?.map { Transport.fromString(it) }
)
fun<T> List<T>.encodeAsCbor(f: (T) -> CBORObject) = CBORObject.NewArray().apply { this@encodeAsCbor.forEach { Add(f(it)) } }
fun<T> Map<String,T>.encodeAsCbor(f: (T) -> CBORObject) = CBORObject.NewMap().apply {
for (entry in this@encodeAsCbor) {
set(entry.key, f(entry.value))
}
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.google.android.gms.fido.fido2.api.common.Algorithm
import com.upokecenter.cbor.CBOREncodeOptions
import com.upokecenter.cbor.CBORObject
import java.math.BigInteger
class CoseKey(
val algorithm: Algorithm,
val x: ByteArray,
val y: ByteArray,
val curveId: Int
) {
constructor(algorithm: Algorithm, x: BigInteger, y: BigInteger, curveId: Int, curvePointSize: Int) :
this(algorithm, x.toByteArray(curvePointSize), y.toByteArray(curvePointSize), curveId)
fun encode(): ByteArray = encodeAsCbor().EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical)
fun encodeAsCbor(): CBORObject = CBORObject.NewMap().apply {
set(KTY, 2.encodeAsCbor())
set(ALG, algorithm.algoValue.encodeAsCbor())
set(CRV, curveId.encodeAsCbor())
set(X, x.encodeAsCbor())
set(Y, y.encodeAsCbor())
}
companion object {
const val KTY = 1
const val ALG = 3
const val CRV = -1
const val X = -2
const val Y = -3
fun BigInteger.toByteArray(size: Int): ByteArray {
val res = ByteArray(size)
val orig = toByteArray()
if (orig.size > size) {
System.arraycopy(orig, orig.size - size, res, 0, size)
} else {
System.arraycopy(orig, 0, res, size - orig.size, orig.size)
}
return res
}
}
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import android.util.Base64
import org.microg.gms.fido.core.digest
import org.microg.gms.utils.toBase64
import java.nio.ByteBuffer
import java.security.PublicKey
class CredentialId(val type: Byte, val data: ByteArray, val rpId: String, val publicKey: PublicKey) {
fun encode(): ByteArray = ByteBuffer.allocate(1 + data.size + 32).apply {
put(type)
put(data)
put((rpId.toByteArray() + publicKey.encoded).digest("SHA-256"))
}.array()
fun toBase64(): String = encode().toBase64(Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
companion object {
fun decodeTypeAndDataByBase64(base64: String): Pair<Byte, ByteArray> {
val bytes = Base64.decode(base64, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
return decodeTypeAndData(bytes)
}
fun decodeTypeAndData(bytes: ByteArray): Pair<Byte, ByteArray> {
val buffer = ByteBuffer.wrap(bytes)
val type = buffer.get()
val data = ByteArray(32)
buffer.get(data)
return type to data
}
}
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.upokecenter.cbor.CBORObject
class FidoU2fAttestationObject(authData: AuthenticatorData, val signature: ByteArray, val attestationCertificate: ByteArray) :
AttestationObject(authData.encode()) {
override val fmt: String
get() = "fido-u2f"
override val attStmt: CBORObject
get() = CBORObject.NewMap().apply {
set("sig", signature.encodeAsCbor())
set("x5c", CBORObject.NewArray().apply { Add(attestationCertificate.encodeAsCbor()) })
}
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol
import com.upokecenter.cbor.CBORObject
class NoneAttestationObject(authData: AuthenticatorData) : AttestationObject(authData.encode()) {
override val fmt: String
get() = "none"
override val attStmt: CBORObject
get() = CBORObject.NewMap()
}

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
fun encodeCommandApdu(
cla: Byte,
ins: Byte,
p1: Byte,
p2: Byte,
data: ByteArray = ByteArray(0),
le: Int = -1,
extended: Boolean = data.size > 255 || le > 255
): ByteArray {
val fixed = byteArrayOf(cla, ins, p1, p2)
val ext = if (extended) byteArrayOf(0) else ByteArray(0)
val req = when {
data.isEmpty() -> ByteArray(0)
extended -> byteArrayOf((data.size shr 8).toByte(), data.size.toByte()) + data
else -> byteArrayOf(data.size.toByte()) + data
}
val res = when {
le == -1 -> ByteArray(0)
extended -> byteArrayOf((le shr 8).toByte(), le.toByte())
else -> byteArrayOf(le.toByte())
}
return fixed + ext + req + res
}
fun decodeResponseApdu(bytes: ByteArray): Pair<Short, ByteArray> {
require(bytes.size >= 2)
return ((bytes[bytes.size - 2].toInt() and 0xff shl 8) + (bytes.last()
.toInt() and 0xff)).toShort() to bytes.sliceArray(0 until bytes.size - 2)
}

View file

@ -0,0 +1,66 @@
package org.microg.gms.fido.core.protocol.msgs
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.CoseKey
import org.microg.gms.fido.core.protocol.decodeAsCoseKey
import org.microg.gms.fido.core.protocol.encodeAsCbor
class AuthenticatorClientPINCommand(request: AuthenticatorClientPINRequest) :
Ctap2Command<AuthenticatorClientPINRequest, AuthenticatorClientPINResponse>(request) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorClientPINResponse.decodeFromCbor(obj)
override val timeout: Long
get() = 60000
}
class AuthenticatorClientPINRequest(
val pinProtocol: Int,
val subCommand: Int,
val keyAgreement: CoseKey? = null,
val pinAuth: ByteArray? = null,
val newPinEnc: ByteArray? = null,
val pinHashEnc: ByteArray? = null
) : Ctap2Request(0x06, CBORObject.NewMap().apply {
set(0x01, pinProtocol.encodeAsCbor())
set(0x02, subCommand.encodeAsCbor())
if (keyAgreement != null) set(0x03, keyAgreement.encodeAsCbor())
if (pinAuth != null) set(0x04, pinAuth.encodeAsCbor())
if (newPinEnc != null) set(0x05, newPinEnc.encodeAsCbor())
if (pinHashEnc != null) set(0x06, pinHashEnc.encodeAsCbor())
}) {
override fun toString(): String {
return "AuthenticatorClientPINRequest(pinProtocol=$pinProtocol, " +
"subCommand=$subCommand, keyAgreement=$keyAgreement, " +
"pinAuth=${pinAuth?.contentToString()}, " +
"newPinEnc=${newPinEnc?.contentToString()}, " +
"pinHashEnc=${pinHashEnc?.contentToString()})"
}
companion object {
// PIN protocol versions
const val PIN_PROTOCOL_VERSION_ONE = 0x01
// PIN subcommands
const val GET_RETRIES = 0x01
const val GET_KEY_AGREEMENT = 0x02
const val SET_PIN = 0x03
const val CHANGE_PIN = 0x04
const val GET_PIN_TOKEN = 0x05
}
}
class AuthenticatorClientPINResponse(
val keyAgreement: CoseKey?,
val pinToken: ByteArray?,
val retries: Int?
) : Ctap2Response {
companion object {
fun decodeFromCbor(obj: CBORObject) = AuthenticatorClientPINResponse(
obj.get(0x01)?.decodeAsCoseKey(),
obj.get(0x02)?.GetByteString(),
obj.get(0x03)?.AsInt32Value()
)
}
}

View file

@ -0,0 +1,81 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.decodeAsPublicKeyCredentialDescriptor
import org.microg.gms.fido.core.protocol.decodeAsPublicKeyCredentialUserEntity
import org.microg.gms.fido.core.protocol.encodeAsCbor
import org.microg.gms.utils.toBase64
class AuthenticatorGetAssertionCommand(request: AuthenticatorGetAssertionRequest) :
Ctap2Command<AuthenticatorGetAssertionRequest, AuthenticatorGetAssertionResponse>(request) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorGetAssertionResponse.decodeFromCbor(obj)
override val timeout: Long
get() = 60000
}
class AuthenticatorGetAssertionRequest(
val rpId: String,
val clientDataHash: ByteArray,
val allowList: List<PublicKeyCredentialDescriptor> = emptyList(),
val extensions: Map<String, CBORObject> = emptyMap(),
val options: Options? = null,
val pinAuth: ByteArray? = null,
val pinProtocol: Int? = null
) : Ctap2Request(0x02, CBORObject.NewMap().apply {
set(0x01, rpId.encodeAsCbor())
set(0x02, clientDataHash.encodeAsCbor())
if (allowList.isNotEmpty()) set(0x03, allowList.encodeAsCbor { it.encodeAsCbor() })
if (extensions.isNotEmpty()) set(0x04, extensions.encodeAsCbor { it })
if (options != null) set(0x05, options.encodeAsCbor())
if (pinAuth != null) set(0x06, pinAuth.encodeAsCbor())
if (pinProtocol != null) set(0x07, pinProtocol.encodeAsCbor())
}) {
override fun toString() = "AuthenticatorGetAssertionRequest(rpId=${rpId}," +
"clientDataHash=0x${clientDataHash.toBase64(Base64.NO_WRAP)}, " +
"allowList=[${allowList.joinToString()}],extensions=[${extensions.entries.joinToString()}]," +
"options=$options,pinAuth=${pinAuth?.toBase64(Base64.NO_WRAP)},pinProtocol=$pinProtocol)"
companion object {
class Options(
val userPresence: Boolean = true,
val userVerification: Boolean = false
) {
fun encodeAsCbor(): CBORObject = CBORObject.NewMap().apply {
// Only encode non-default values
if (!userPresence) set("up", userPresence.encodeAsCbor())
if (userVerification) set("uv", userVerification.encodeAsCbor())
}
override fun toString(): String {
return "(userPresence=$userPresence, userVerification=$userVerification)"
}
}
}
}
class AuthenticatorGetAssertionResponse(
val credential: PublicKeyCredentialDescriptor?,
val authData: ByteArray,
val signature: ByteArray,
val user: PublicKeyCredentialUserEntity?,
val numberOfCredentials: Int?
) : Ctap2Response {
companion object {
fun decodeFromCbor(obj: CBORObject) = AuthenticatorGetAssertionResponse(
credential = obj.get(0x01)?.decodeAsPublicKeyCredentialDescriptor(),
authData = obj.get(0x02).GetByteString(),
signature = obj.get(0x03).GetByteString(),
user = obj.get(0x04)?.decodeAsPublicKeyCredentialUserEntity(),
numberOfCredentials = obj.get(0x05)?.AsInt32Value()
)
}
}

View file

@ -0,0 +1,167 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.AsInt32Sequence
import org.microg.gms.fido.core.protocol.AsStringSequence
import org.microg.gms.fido.core.protocol.decodeAsPublicKeyCredentialParameters
import org.microg.gms.utils.ToStringHelper
class AuthenticatorGetInfoCommand : Ctap2Command<AuthenticatorGetInfoRequest, AuthenticatorGetInfoResponse>(AuthenticatorGetInfoRequest()) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorGetInfoResponse.decodeFromCbor(obj)
}
class AuthenticatorGetInfoRequest : Ctap2Request(0x04)
class AuthenticatorGetInfoResponse(
val versions: List<String>,
val extensions: List<String>,
val aaguid: ByteArray,
val options: Options,
val maxMsgSize: Int?,
val pinUvAuthProtocols: List<Int>,
val maxCredentialCountInList: Int?,
val maxCredentialIdLength: Int?,
val transports: List<String>?,
val algorithms: List<PublicKeyCredentialParameters>?,
val maxSerializedLargeBlobArray: Int?,
val forcePINChange: Boolean,
val minPINLength: Int?,
val firmwareVersion: Int?,
val maxCredBlobLength: Int?,
val maxRPIDsForSetMinPINLength: Int?,
val preferredPlatformUvAttempts: Int?,
val uvModality: Int?,
val certifications: Map<String, Int>?,
val remainingDiscoverableCredentials: Int?,
val vendorPrototypeConfigCommands: List<Int>?,
) : Ctap2Response {
companion object {
class Options(
val platformDevice: Boolean,
val residentKey: Boolean,
val clientPin: Boolean?,
val userPresence: Boolean,
val userVerification: Boolean?,
val pinUvAuthToken: Boolean?,
val noMcGaPermissionsWithClientPin: Boolean,
val largeBlobs: Boolean?,
val enterpriseAttestation: Boolean?,
val bioEnroll: Boolean?,
val userVerificationMgmtPreview: Boolean?,
val uvBioEnroll: Boolean?,
val authenticatorConfigSupported: Boolean?,
val uvAcfg: Boolean?,
val credentialManagementSupported: Boolean?,
val credentialMgmtPreview: Boolean?,
val setMinPINLengthSupported: Boolean?,
val makeCredUvNotRqd: Boolean,
val alwaysUv: Boolean?,
) {
companion object {
fun decodeFromCbor(map: CBORObject?) = Options(
platformDevice = map?.get("plat")?.AsBoolean() == true,
residentKey = map?.get("rk")?.AsBoolean() == true,
clientPin = map?.get("clientPin")?.AsBoolean(),
userPresence = map?.get("up")?.AsBoolean() != false,
userVerification = map?.get("uv")?.AsBoolean(),
pinUvAuthToken = map?.get("pinUvAuthToken")?.AsBoolean(),
noMcGaPermissionsWithClientPin = map?.get("noMcGaPermissionsWithClientPin")?.AsBoolean() == true,
largeBlobs = map?.get("largeBlobs")?.AsBoolean(),
enterpriseAttestation = map?.get("ep")?.AsBoolean(),
bioEnroll = map?.get("bioEnroll")?.AsBoolean(),
userVerificationMgmtPreview = map?.get("userVerificationMgmtPreview")?.AsBoolean(),
uvBioEnroll = map?.get("uvBioEnroll")?.AsBoolean(),
authenticatorConfigSupported = map?.get("authnrCfg")?.AsBoolean(),
uvAcfg = map?.get("uvAcfg")?.AsBoolean(),
credentialManagementSupported = map?.get("credMgmt")?.AsBoolean(),
credentialMgmtPreview = map?.get("credentialMgmtPreview")?.AsBoolean(),
setMinPINLengthSupported = map?.get("setMinPINLength")?.AsBoolean(),
makeCredUvNotRqd = map?.get("makeCredUvNotRqd")?.AsBoolean() == true,
alwaysUv = map?.get("alwaysUv")?.AsBoolean(),
)
}
override fun toString(): String {
return ToStringHelper.name("Options")
.field("platformDevice", platformDevice)
.field("residentKey", residentKey)
.field("clientPin", clientPin)
.field("userPresence", userPresence)
.field("userVerification", userVerification)
.field("pinUvAuthToken", pinUvAuthToken)
.field("noMcGaPermissionsWithClientPin", noMcGaPermissionsWithClientPin)
.field("largeBlobs", largeBlobs)
.field("enterpriseAttestation", enterpriseAttestation)
.field("bioEnroll", bioEnroll)
.field("userVerificationMgmtPreview", userVerificationMgmtPreview)
.field("uvBioEnroll", uvBioEnroll)
.field("authenticatorConfigSupported", authenticatorConfigSupported)
.field("uvAcfg", uvAcfg)
.field("credentialManagementSupported", credentialManagementSupported)
.field("credentialMgmtPreview", credentialMgmtPreview)
.field("setMinPINLengthSupported", setMinPINLengthSupported)
.field("makeCredUvNotRqd", makeCredUvNotRqd)
.field("alwaysUv", alwaysUv)
.end()
}
}
fun decodeFromCbor(obj: CBORObject) = AuthenticatorGetInfoResponse(
versions = obj.get(1)?.AsStringSequence()?.toList().orEmpty(),
extensions = obj.get(2)?.AsStringSequence()?.toList().orEmpty(),
aaguid = obj.get(3)?.GetByteString()
?: throw IllegalArgumentException("Not a valid response for authenticatorGetInfo"),
options = Options.decodeFromCbor(obj.get(4)),
maxMsgSize = obj.get(5)?.AsInt32Value(),
pinUvAuthProtocols = obj.get(6)?.AsInt32Sequence()?.toList().orEmpty(),
maxCredentialCountInList = obj.get(7)?.AsInt32Value(),
maxCredentialIdLength = obj.get(8)?.AsInt32Value(),
transports = obj.get(9)?.AsStringSequence()?.toList(),
algorithms = runCatching { obj.get(10)?.values?.map { it.decodeAsPublicKeyCredentialParameters() } }.getOrNull(),
maxSerializedLargeBlobArray = obj.get(11)?.AsInt32Value(),
forcePINChange = obj.get(12)?.AsBoolean() == true,
minPINLength = obj.get(13)?.AsInt32Value(),
firmwareVersion = obj.get(14)?.AsInt32Value(),
maxCredBlobLength = obj.get(15)?.AsInt32Value(),
maxRPIDsForSetMinPINLength = obj.get(16)?.AsInt32Value(),
preferredPlatformUvAttempts = obj.get(17)?.AsInt32Value(),
uvModality = obj.get(18)?.AsInt32Value(),
certifications = obj.get(19)?.entries?.mapNotNull { runCatching { it.key.AsString() to it.value.AsInt32Value() }.getOrNull() }?.toMap(),
remainingDiscoverableCredentials = obj.get(20)?.AsInt32Value(),
vendorPrototypeConfigCommands = obj.get(21)?.AsInt32Sequence()?.toList(),
)
}
override fun toString(): String {
return ToStringHelper.name("AuthenticatorGetInfoResponse")
.field("versions", versions)
.field("extensions", extensions)
.field("aaguid", aaguid)
.field("options", options)
.field("maxMsgSize", maxMsgSize)
.field("pinUvAuthProtocols", pinUvAuthProtocols)
.field("maxCredentialCountInList", maxCredentialCountInList)
.field("maxCredentialIdLength", maxCredentialIdLength)
.field("transports", transports)
.field("algorithms", algorithms)
.field("maxSerializedLargeBlobArray", maxSerializedLargeBlobArray)
.field("forcePINChange", forcePINChange)
.field("minPINLength", minPINLength)
.field("firmwareVersion", firmwareVersion)
.field("maxCredBlobLength", maxCredBlobLength)
.field("maxRPIDsForSetMinPINLength", maxRPIDsForSetMinPINLength)
.field("preferredPlatformUvAttempts", preferredPlatformUvAttempts)
.field("uvModality", uvModality)
.field("certifications", certifications)
.field("remainingDiscoverableCredentials", remainingDiscoverableCredentials)
.field("vendorPrototypeConfigCommands", vendorPrototypeConfigCommands)
.end()
}
}

View file

@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.encodeAsCbor
import org.microg.gms.utils.toBase64
class AuthenticatorMakeCredentialCommand(request: AuthenticatorMakeCredentialRequest) :
Ctap2Command<AuthenticatorMakeCredentialRequest, AuthenticatorMakeCredentialResponse>(request) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorMakeCredentialResponse.decodeFromCbor(obj)
override val timeout: Long
get() = 60000
}
class AuthenticatorMakeCredentialRequest(
val clientDataHash: ByteArray,
val rp: PublicKeyCredentialRpEntity,
val user: PublicKeyCredentialUserEntity,
val pubKeyCredParams: List<PublicKeyCredentialParameters>,
val excludeList: List<PublicKeyCredentialDescriptor> = emptyList(),
val extensions: Map<String, CBORObject> = emptyMap(),
val options: Options? = null,
val pinAuth: ByteArray? = null,
val pinProtocol: Int? = null
) : Ctap2Request(0x01, CBORObject.NewMap().apply {
set(0x01, clientDataHash.encodeAsCbor())
set(0x02, rp.encodeAsCbor())
set(0x03, user.encodeAsCbor())
set(0x04, pubKeyCredParams.encodeAsCbor { it.encodeAsCbor() })
if (excludeList.isNotEmpty()) set(0x05, excludeList.encodeAsCbor { it.encodeAsCbor() })
if (extensions.isNotEmpty()) set(0x06, extensions.encodeAsCbor { it })
if (options != null) set(0x07, options.encodeAsCbor())
if (pinAuth != null) set(0x08, pinAuth.encodeAsCbor())
if (pinProtocol != null) set(0x09, pinProtocol.encodeAsCbor())
}) {
override fun toString() = "AuthenticatorMakeCredentialRequest(clientDataHash=0x${clientDataHash.toBase64(Base64.NO_WRAP)}, " +
"rp=$rp,user=$user,pubKeyCredParams=[${pubKeyCredParams.joinToString()}]," +
"excludeList=[${excludeList.joinToString()}],extensions=[${extensions.entries.joinToString()}]," +
"options=$options,pinAuth=${pinAuth?.toBase64(Base64.NO_WRAP)},pinProtocol=$pinProtocol)"
companion object {
class Options(
val residentKey: Boolean = false,
val userVerification: Boolean = false
) {
fun encodeAsCbor() = CBORObject.NewMap().apply {
// Only encode non-default values
if (residentKey) set("rk", residentKey.encodeAsCbor())
if (userVerification) set("uv", userVerification.encodeAsCbor())
}
}
}
}
class AuthenticatorMakeCredentialResponse(
val authData: ByteArray,
val fmt: String,
val attStmt: CBORObject
) : Ctap2Response {
companion object {
fun decodeFromCbor(obj: CBORObject) = AuthenticatorMakeCredentialResponse(
fmt = obj.get(0x01).AsString(),
authData = obj.get(0x02).GetByteString(),
attStmt = obj.get(0x03)
)
}
}

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import org.microg.gms.utils.toBase64
import java.io.ByteArrayInputStream
import java.io.InputStream
abstract class Ctap1Command<Q : Ctap1Request, S : Ctap1Response>(val request: Q) {
val commandByte: Byte
get() = request.commandByte
fun decodeResponse(statusCode: Short, bytes: ByteArray, offset: Int = 0): S =
decodeResponse(statusCode, ByteArrayInputStream(bytes, offset, bytes.size - offset))
abstract fun decodeResponse(statusCode: Short, i: InputStream): S
}
abstract class Ctap1Request(
val commandByte: Byte,
val p1: Byte = 0,
val p2: Byte = 0,
val data: ByteArray
) {
val apdu = encodeCommandApdu(0, commandByte, p1, p2, data, extended = true)
override fun toString(): String = "Ctap1Request(command=0x${commandByte.toString(16)}, " +
"p1=0x${p1.toString(16)}, " +
"p2=0x${p2.toString(16)}, " +
"data=${data.toBase64(Base64.NO_WRAP)})"
}
abstract class Ctap1Response(val statusCode: Short) {
open fun encode(): ByteArray = throw UnsupportedOperationException()
}

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import com.upokecenter.cbor.CBOREncodeOptions
import com.upokecenter.cbor.CBORObject
import org.microg.gms.utils.toBase64
import java.io.ByteArrayInputStream
import java.io.InputStream
abstract class Ctap2Command<Q: Ctap2Request, R: Ctap2Response>(val request: Q) {
val hasParameters: Boolean
get() = request.parameters != null
open val timeout: Long
get() = 1000
fun decodeResponse(bytes: ByteArray, offset: Int = 0): R =
decodeResponse(ByteArrayInputStream(bytes, offset, bytes.size - offset))
open fun decodeResponse(i: InputStream) = decodeResponse(CBORObject.Read(i))
abstract fun decodeResponse(obj: CBORObject): R
}
interface Ctap2Response
abstract class Ctap2Request(val commandByte: Byte, val parameters: CBORObject? = null) {
val payload: ByteArray = parameters?.EncodeToBytes(CBOREncodeOptions.DefaultCtap2Canonical) ?: ByteArray(0)
override fun toString(): String = "Ctap2Request(command=0x${commandByte.toString(16)}, " +
"payload=${payload.toBase64(Base64.NO_WRAP)})"
}

View file

@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import org.microg.gms.utils.toBase64
import java.io.DataInputStream
import java.io.InputStream
class U2fAuthenticationCommand(request: U2fAuthenticationRequest) :
Ctap1Command<U2fAuthenticationRequest, U2fAuthenticationResponse>(request) {
constructor(controlByte: Byte, challenge: ByteArray, application: ByteArray, keyHandle: ByteArray) :
this(U2fAuthenticationRequest(controlByte, challenge, application, keyHandle))
override fun decodeResponse(statusCode: Short, i: InputStream): U2fAuthenticationResponse = U2fAuthenticationResponse.decode(statusCode, i)
}
class U2fAuthenticationRequest(val controlByte: Byte, val challenge: ByteArray, val application: ByteArray, val keyHandle: ByteArray) :
Ctap1Request(0x02, data = challenge + application + keyHandle.size.toByte() + keyHandle, p1 = controlByte) {
init {
require(challenge.size == 32)
require(application.size == 32)
}
override fun toString(): String = "U2fAuthenticationRequest(controlByte=0x${controlByte.toString(16)}, " +
"challenge=${challenge.toBase64(Base64.NO_WRAP)}, " +
"application=${application.toBase64(Base64.NO_WRAP)}, " +
"keyHandle=${keyHandle.toBase64(Base64.NO_WRAP)})"
}
class U2fAuthenticationResponse(
statusCode: Short,
val userPresence: Boolean,
val counter: Int,
val signature: ByteArray
) : Ctap1Response(statusCode) {
companion object {
fun decode(statusCode: Short, i: InputStream): U2fAuthenticationResponse {
val userPresence = i.read() and 0x1 > 0
val counter = DataInputStream(i).readInt()
val signature = i.readBytes()
return U2fAuthenticationResponse(statusCode, userPresence, counter, signature)
}
}
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.protocol.msgs
import android.util.Base64
import org.microg.gms.utils.toBase64
import java.io.InputStream
import javax.security.cert.X509Certificate
class U2fRegistrationCommand(request: U2fRegistrationRequest) :
Ctap1Command<U2fRegistrationRequest, U2fRegistrationResponse>(request) {
constructor(challenge: ByteArray, application: ByteArray) : this(U2fRegistrationRequest(challenge, application))
override fun decodeResponse(statusCode: Short, i: InputStream): U2fRegistrationResponse = U2fRegistrationResponse.decode(statusCode, i)
}
class U2fRegistrationRequest(val challenge: ByteArray, val application: ByteArray) :
Ctap1Request(0x01, data = challenge + application) {
init {
require(challenge.size == 32)
require(application.size == 32)
}
override fun toString(): String = "U2fRegistrationRequest(challenge=${challenge.toBase64(Base64.NO_WRAP)}, " +
"application=${application.toBase64(Base64.NO_WRAP)})"
}
class U2fRegistrationResponse(
statusCode: Short,
val userPublicKey: ByteArray,
val keyHandle: ByteArray,
val attestationCertificate: ByteArray,
val signature: ByteArray
) : Ctap1Response(statusCode) {
companion object {
fun decode(statusCode: Short, i: InputStream): U2fRegistrationResponse {
require(i.read() == 0x05) // reserved byte
val userPublicKey = ByteArray(65)
i.read(userPublicKey)
val keyHandle = ByteArray(i.read())
i.read(keyHandle)
val attestationCertificate = X509Certificate.getInstance(i).encoded
val signature = i.readBytes()
return U2fRegistrationResponse(statusCode, userPublicKey, keyHandle, attestationCertificate, signature)
}
}
}

View file

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.regular
import android.app.KeyguardManager
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Context.KEYGUARD_SERVICE
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Parcel
import androidx.core.app.PendingIntentCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
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 com.google.android.gms.fido.fido2.api.IBooleanCallback
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions
import com.google.android.gms.fido.fido2.internal.regular.IFido2AppCallbacks
import com.google.android.gms.fido.fido2.internal.regular.IFido2AppService
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.common.GmsService.FIDO2_REGULAR
import org.microg.gms.fido.core.FEATURES
import org.microg.gms.fido.core.ui.AuthenticatorActivity
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.SOURCE_APP
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SOURCE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_OPTIONS
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SERVICE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_TYPE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_REGISTER
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_SIGN
import org.microg.gms.utils.warnOnTransactionIssues
const val TAG = "Fido2Regular"
class Fido2AppService : BaseService(TAG, FIDO2_REGULAR) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS,
Fido2AppServiceImpl(this, lifecycle).asBinder(),
ConnectionInfo().apply { features = FEATURES }
);
}
}
class Fido2AppServiceImpl(private val context: Context, override val lifecycle: Lifecycle) :
IFido2AppService.Stub(), LifecycleOwner {
override fun getRegisterPendingIntent(callbacks: IFido2AppCallbacks, options: PublicKeyCredentialCreationOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_REGULAR.SERVICE_ID)
.putExtra(KEY_SOURCE, SOURCE_APP)
.putExtra(KEY_TYPE, TYPE_REGISTER)
.putExtra(KEY_OPTIONS, options.serializeToBytes())
val pendingIntent =
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
}
}
override fun getSignPendingIntent(callbacks: IFido2AppCallbacks, options: PublicKeyCredentialRequestOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_REGULAR.SERVICE_ID)
.putExtra(KEY_SOURCE, SOURCE_APP)
.putExtra(KEY_TYPE, TYPE_SIGN)
.putExtra(KEY_OPTIONS, options.serializeToBytes())
val pendingIntent =
PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false)
callbacks.onPendingIntent(Status.SUCCESS, pendingIntent)
}
}
override fun isUserVerifyingPlatformAuthenticatorAvailable(callbacks: IBooleanCallback) {
lifecycleScope.launchWhenStarted {
if (SDK_INT < 24) {
callbacks.onBoolean(false)
} else {
val keyguardManager = context.getSystemService(KEYGUARD_SERVICE) as? KeyguardManager?
callbacks.onBoolean(keyguardManager?.isDeviceSecure == true)
}
}
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) }
}

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport
import com.google.android.gms.fido.fido2.api.common.ErrorCode
import org.microg.gms.fido.core.RequestHandlingException
import org.microg.gms.fido.core.protocol.msgs.*
const val CAPABILITY_CTAP_1 = 1 shl 0
const val CAPABILITY_CTAP_2 = 1 shl 1
const val CAPABILITY_CTAP_2_1 = 1 shl 2
const val CAPABILITY_CLIENT_PIN = 1 shl 3
const val CAPABILITY_WINK = 1 shl 4
const val CAPABILITY_MAKE_CRED_WITHOUT_UV = 1 shl 5
const val CAPABILITY_USER_VERIFICATION = 1 shl 6
const val CAPABILITY_RESIDENT_KEY = 1 shl 7
interface CtapConnection {
val capabilities: Int
val transports: List<String>
val hasCtap1Support: Boolean
get() = capabilities and CAPABILITY_CTAP_1 > 0
val hasCtap2Support: Boolean
get() = capabilities and CAPABILITY_CTAP_2 > 0
val hasCtap21Support: Boolean
get() = capabilities and CAPABILITY_CTAP_2_1 > 0
val hasClientPin: Boolean
get() = capabilities and CAPABILITY_CLIENT_PIN > 0
val hasWinkSupport: Boolean
get() = capabilities and CAPABILITY_WINK > 0
val canMakeCredentialWithoutUserVerification: Boolean
get() = capabilities and CAPABILITY_MAKE_CRED_WITHOUT_UV > 0
val hasUserVerificationSupport: Boolean
get() = capabilities and CAPABILITY_USER_VERIFICATION > 0
val hasResidentKey: Boolean
get() = capabilities and CAPABILITY_RESIDENT_KEY > 0
suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S
suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S
}
class Ctap2StatusException(val status: Byte) : Exception("Received status ${(status.toInt() and 0xff).toString(16)}")

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport
enum class Transport {
BLUETOOTH,
NFC,
USB,
SCREEN_LOCK
}

View file

@ -0,0 +1,542 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.annotation.RequiresApi
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.ResidentKeyRequirement.*
import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement.*
import com.upokecenter.cbor.CBORObject
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.protocol.*
import org.microg.gms.fido.core.protocol.CoseKey.Companion.toByteArray
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.nfc.CtapNfcMessageStatusException
import org.microg.gms.fido.core.transport.usb.ctaphid.CtapHidMessageStatusException
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.AlgorithmParameters
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.interfaces.ECPublicKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.ECParameterSpec
import java.security.spec.ECPoint
import java.security.spec.ECPublicKeySpec
import javax.crypto.Cipher
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
abstract class TransportHandler(val transport: Transport, val callback: TransportHandlerCallback?) {
open val isSupported: Boolean
get() = false
open suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean = false, pin: String? = null, userInfo: String? = null): AuthenticatorResponse =
throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR)
open fun shouldBeUsedInstantly(options: RequestOptions): Boolean = false
fun invokeStatusChanged(status: String, extras: Bundle? = null) =
callback?.onStatusChanged(transport, status, extras)
private suspend fun ctap1DeviceHasCredential(
connection: CtapConnection,
challenge: ByteArray,
application: ByteArray,
descriptor: PublicKeyCredentialDescriptor
): Boolean {
try {
connection.runCommand(U2fAuthenticationCommand(0x07, challenge, application, descriptor.id))
return true
} catch (e: CtapHidMessageStatusException) {
return e.status == 0x6985;
} catch (e: CtapNfcMessageStatusException) {
return e.status == 0x6985;
}
}
private suspend fun ctap2register(
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray,
requireResidentKey: Boolean,
requireUserVerification: Boolean,
pinToken: ByteArray? = null
): Pair<AuthenticatorMakeCredentialResponse, ByteArray?> {
// The CTAP2 spec states that the requireUserVerification option from WebAuthn should map
// to the "uv" option OR the pinAuth/pinProtocl options in the CTAP standard.
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo
// Later drafts of the standard are much more explicit about this, and state that platforms
// MUST NOT include the "uv" option key if the authenticator does not support built-in
// verification, and that they MUST NOT include both the "uv" option key and the pinUvAuth
// parameter in the same request
// https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#authenticatorMakeCredential
val ctap2RequireVerification = requireUserVerification && (pinToken == null)
val reqOptions = AuthenticatorMakeCredentialRequest.Companion.Options(
requireResidentKey,
ctap2RequireVerification
)
val extensions = mutableMapOf<String, CBORObject>()
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
extensions["appidExclude"] =
options.authenticationExtensions!!.fidoAppIdExtension!!.appId.encodeAsCbor()
}
if (options.authenticationExtensions?.userVerificationMethodExtension?.uvm != null) {
extensions["uvm"] =
options.authenticationExtensions!!.userVerificationMethodExtension!!.uvm.encodeAsCbor()
}
var pinProtocol: Int? = null
var pinHashEnc: ByteArray? = null
if (pinToken != null) {
val secretKeySpec = SecretKeySpec(pinToken, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(secretKeySpec)
pinHashEnc = mac.doFinal(clientDataHash).sliceArray(IntRange(0, 15))
pinProtocol = 1
}
val request = AuthenticatorMakeCredentialRequest(
clientDataHash,
options.registerOptions.rp,
options.registerOptions.user,
options.registerOptions.parameters,
options.registerOptions.excludeList.orEmpty(),
extensions,
reqOptions,
pinHashEnc,
pinProtocol
)
val response = connection.runCommand(AuthenticatorMakeCredentialCommand(request))
val credentialId = AuthenticatorData.decode(response.authData).attestedCredentialData?.id
return response to credentialId
}
private suspend fun ctap1register(
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray
): Pair<AuthenticatorMakeCredentialResponse, ByteArray> {
val rpIdHash = options.rpId.toByteArray().digest("SHA-256")
val appIdHash =
options.authenticationExtensions?.fidoAppIdExtension?.appId?.toByteArray()?.digest("SHA-256")
if (!options.registerOptions.parameters.isNullOrEmpty() && options.registerOptions.parameters.all { it.algorithmIdAsInteger != -7 })
throw IllegalArgumentException("Can't use CTAP1 protocol for non ES256 requests")
if (options.registerOptions.authenticatorSelection?.requireResidentKey == true)
throw IllegalArgumentException("Can't use CTAP1 protocol when resident key required")
val hasCredential = options.registerOptions.excludeList.orEmpty().any { cred ->
ctap1DeviceHasCredential(connection, clientDataHash, rpIdHash, cred) ||
if (appIdHash != null) {
ctap1DeviceHasCredential(connection, clientDataHash, appIdHash, cred)
} else {
false
}
}
while (true) {
try {
val response = connection.runCommand(U2fRegistrationCommand(clientDataHash, rpIdHash))
if (hasCredential) throw RequestHandlingException(
ErrorCode.NOT_ALLOWED_ERR,
"An excluded credential has already been registered with the device"
)
require(response.userPublicKey[0] == 0x04.toByte())
val coseKey = CoseKey(
EC2Algorithm.ES256,
response.userPublicKey.sliceArray(1 until 33),
response.userPublicKey.sliceArray(33 until 65),
1
)
val credentialData =
AttestedCredentialData(ByteArray(16), response.keyHandle, coseKey.encode())
val authData = AuthenticatorData(
options.rpId.toByteArray().digest("SHA-256"),
true,
false,
0,
credentialData
)
val attestationObject = if (options.registerOptions.skipAttestation) {
NoneAttestationObject(authData)
} else {
FidoU2fAttestationObject(authData, response.signature, response.attestationCertificate)
}
val ctap2Response = AuthenticatorMakeCredentialResponse(
authData.encode(),
attestationObject.fmt,
attestationObject.attStmt
)
return ctap2Response to response.keyHandle
} catch (e: CtapHidMessageStatusException) {
if (e.status != 0x6985) {
throw e
}
}
delay(100)
}
}
internal suspend fun register(
connection: CtapConnection,
context: Context,
options: RequestOptions,
callerPackage: String,
pinRequested: Boolean,
pin: String?
): AuthenticatorAttestationResponse {
val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
val requireResidentKey = when (options.registerOptions.authenticatorSelection?.residentKeyRequirement) {
RESIDENT_KEY_REQUIRED -> true
RESIDENT_KEY_PREFERRED -> connection.hasResidentKey
RESIDENT_KEY_DISCOURAGED -> false
// If residentKeyRequirement is not set, use the value for requireResidentKey
// Default value for requireResidentKey is false
else -> options.registerOptions.authenticatorSelection?.requireResidentKey == true
}
val requireUserVerification = when(options.registerOptions.authenticatorSelection?.requireUserVerification) {
REQUIRED -> true
DISCOURAGED -> false
// PREFERRED is the default, according to the standard
// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification
// If preferred, only return true if connection is capable of user verification
else -> connection.hasClientPin || connection.hasUserVerificationSupport
}
// If the authenticator has a built-in verification method, let that take precedence over
// client PIN
val requiresPin = requireUserVerification && !connection.hasUserVerificationSupport && connection.hasClientPin
val (response, keyHandle) = when {
connection.hasCtap2Support && (requireResidentKey || requiresPin) -> {
try {
var pinToken: ByteArray? = null
// If we previously requested a pin and the user cancelled it (ie. pinRequested
// is true and pin is still null), don't throw the exception, and pass the request
// to the authenticator without a pin.
if (requiresPin && !pinRequested && pin == null) {
throw MissingPinException()
}
if (requiresPin && pin != null && SDK_INT >= 23) {
pinToken = ctap2getPinToken(connection, pin)
}
// Authenticators seem to give a response even without a PIN token, so we'll allow
// the client to call this even without having a PIN token set
ctap2register(connection, options, clientDataHash, requireResidentKey, requireUserVerification, pinToken)
} catch (e: Ctap2StatusException) {
if (e.status == 0x36.toByte()) {
throw MissingPinException()
} else if (e.status == 0x31.toByte()) {
throw WrongPinException()
} else {
throw e
}
}
}
connection.hasCtap1Support -> ctap1register(connection, options, clientDataHash)
else -> throw IllegalStateException()
}
return AuthenticatorAttestationResponse(
keyHandle ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") },
clientData,
AnyAttestationObject(response.authData, response.fmt, response.attStmt).encode(),
connection.transports.toTypedArray()
)
}
private suspend fun ctap2sign(
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray,
requireUserVerification: Boolean,
pinToken: ByteArray? = null
): Pair<AuthenticatorGetAssertionResponse, ByteArray?> {
val reqOptions = AuthenticatorGetAssertionRequest.Companion.Options(
// The specification states that the WebAuthn requireUserVerification option should map to
// the CTAP2 "uv" flag OR pinAuth/pinProtocol. Therefore, set this flag to false if
// a pinToken is present
userVerification = requireUserVerification && (pinToken == null)
)
val extensions = mutableMapOf<String, CBORObject>()
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
extensions["appid"] = options.authenticationExtensions!!.fidoAppIdExtension!!.appId.encodeAsCbor()
}
if (options.authenticationExtensions?.userVerificationMethodExtension?.uvm != null) {
extensions["uvm"] =
options.authenticationExtensions!!.userVerificationMethodExtension!!.uvm.encodeAsCbor()
}
var pinProtocol: Int? = null
var pinHashEnc: ByteArray? = null
if (pinToken != null) {
val secretKeySpec = SecretKeySpec(pinToken, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(secretKeySpec)
pinHashEnc = mac.doFinal(clientDataHash).sliceArray(IntRange(0, 15))
pinProtocol = 1
}
val request = AuthenticatorGetAssertionRequest(
options.rpId,
clientDataHash,
options.signOptions.allowList.orEmpty(),
extensions,
reqOptions,
pinHashEnc,
pinProtocol
)
val ctap2Response = connection.runCommand(AuthenticatorGetAssertionCommand(request))
return ctap2Response to ctap2Response.credential?.id
}
@RequiresApi(23)
private suspend fun ctap2getPinToken(
connection: CtapConnection,
pin: String
): ByteArray? {
// Ask for shared secret from authenticator
val sharedSecretRequest = AuthenticatorClientPINRequest(
AuthenticatorClientPINRequest.PIN_PROTOCOL_VERSION_ONE,
AuthenticatorClientPINRequest.GET_KEY_AGREEMENT
)
val sharedSecretResponse = connection.runCommand(AuthenticatorClientPINCommand(sharedSecretRequest))
if (sharedSecretResponse.keyAgreement == null) {
return null;
}
val x = sharedSecretResponse.keyAgreement.x
val y = sharedSecretResponse.keyAgreement.y
val curveName = when (sharedSecretResponse.keyAgreement.curveId) {
1 -> "secp256r1"
2 -> "secp384r1"
3 -> "secp521r1"
4 -> "x25519"
5 -> "x448"
6 -> "Ed25519"
7 -> "Ed448"
else -> return null
}
// Perform Diffie Hellman key generation
val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC)
generator.initialize(ECGenParameterSpec(curveName))
val myKeyPair = generator.generateKeyPair()
val parameters = AlgorithmParameters.getInstance("EC")
parameters.init(ECGenParameterSpec(curveName))
val parameterSpec = parameters.getParameterSpec(ECParameterSpec::class.java)
val serverKey = KeyFactory.getInstance("EC")
.generatePublic(ECPublicKeySpec(ECPoint(BigInteger(1, x), BigInteger(1, y)), parameterSpec))
val keyAgreement = KeyAgreement.getInstance("ECDH")
keyAgreement.init(myKeyPair.private)
keyAgreement.doPhase(serverKey, true)
// We get the key for the encryption used between the client and the platform by doing an
// SHA 256 hash of the shared secret
val sharedSecret = keyAgreement.generateSecret()
val hash = MessageDigest.getInstance("SHA-256")
hash.update(sharedSecret)
val sharedKey = SecretKeySpec(hash.digest(), "AES")
// Hash the PIN, and then encrypt the first 16 bytes of the hash using the shared key
val pinHash = MessageDigest.getInstance("SHA-256").digest(pin.toByteArray(StandardCharsets.UTF_8))
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, sharedKey, IvParameterSpec(ByteArray(16)))
val pinHashEnc = cipher.doFinal(pinHash.sliceArray(IntRange(0,15)))
// Now, send back the encrypted pin hash, as well as the public portion of our key so
// the authenticator also may perform Diffie Hellman
val publicKey = myKeyPair.public
if (publicKey !is ECPublicKey) {
return null
}
val coseKey = CoseKey(
sharedSecretResponse.keyAgreement.algorithm,
publicKey.w.affineX.toByteArray(32),
publicKey.w.affineY.toByteArray(32),
sharedSecretResponse.keyAgreement.curveId
)
val pinTokenRequest = AuthenticatorClientPINRequest(
AuthenticatorClientPINRequest.PIN_PROTOCOL_VERSION_ONE,
AuthenticatorClientPINRequest.GET_PIN_TOKEN,
coseKey,
pinHashEnc = pinHashEnc
)
// The pin token is returned to us in encrypted form. Decrypt it, so we may use it when HMAC
// signing later
val pinTokenResponse = connection.runCommand(AuthenticatorClientPINCommand(pinTokenRequest))
cipher.init(Cipher.DECRYPT_MODE, sharedKey, IvParameterSpec(ByteArray(16)))
return cipher.doFinal(pinTokenResponse.pinToken)
}
private suspend fun ctap1sign(
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray,
rpIdHash: ByteArray
): Pair<AuthenticatorGetAssertionResponse, ByteArray> {
val cred = options.signOptions.allowList.orEmpty().firstOrNull { cred ->
ctap1DeviceHasCredential(connection, clientDataHash, rpIdHash, cred)
} ?: options.signOptions.allowList!!.first()
while (true) {
try {
val response = connection.runCommand(U2fAuthenticationCommand(0x03, clientDataHash, rpIdHash, cred.id))
val authData = AuthenticatorData(rpIdHash, response.userPresence, false, response.counter)
val ctap2Response = AuthenticatorGetAssertionResponse(
cred,
authData.encode(),
response.signature,
null,
null
)
return ctap2Response to cred.id
} catch (e: CtapHidMessageStatusException) {
if (e.status != 0x6985) {
throw e
}
delay(100)
}
}
}
private suspend fun ctap1sign(
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray
): Pair<AuthenticatorGetAssertionResponse, ByteArray> {
try {
val rpIdHash = options.rpId.toByteArray().digest("SHA-256")
return ctap1sign(connection, options, clientDataHash, rpIdHash)
} catch (e: Exception) {
try {
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
val appIdHash = options.authenticationExtensions!!.fidoAppIdExtension!!.appId.toByteArray()
.digest("SHA-256")
return ctap1sign(connection, options, clientDataHash, appIdHash)
}
} catch (e2: Exception) {
}
// Throw original
throw e
}
}
internal suspend fun sign(
connection: CtapConnection,
context: Context,
options: RequestOptions,
callerPackage: String,
pinRequested: Boolean,
pin: String?
): AuthenticatorAssertionResponse {
val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
val (response, credentialId) = when {
connection.hasCtap2Support -> {
try {
var pinToken: ByteArray? = null
val requireUserVerification = when(options.signOptions.requireUserVerification) {
REQUIRED -> true
DISCOURAGED -> false
// PREFERRED is the default, according to the standard
// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification
else -> {
// If preferred, only return true if connection is capable of user verification
connection.hasClientPin || connection.hasUserVerificationSupport
}
}
// If the authenticator has built in user verification, let that take precedence
// over PIN verification
val requiresPin = requireUserVerification && !connection.hasUserVerificationSupport && connection.hasClientPin
// If we require a PIN, throw an exception up to the AuthenticatorActivity
// However, if we've already asked the user for a PIN and the user cancelled
// (ie. pinRequested is true), continue without asking
if (requiresPin && !pinRequested && pin == null) {
throw MissingPinException()
}
if (requiresPin && pin != null && SDK_INT >= 23) {
pinToken = ctap2getPinToken(connection, pin)
}
// Authenticators seem to give a response even without a PIN token, so we'll allow
// the client to call this even without having a PIN token set
ctap2sign(connection, options, clientDataHash, requireUserVerification, pinToken)
} catch (e: Ctap2StatusException) {
if (e.status == 0x31.toByte()) {
throw WrongPinException()
} else if (e.status == 0x36.toByte()) {
throw MissingPinException()
} else if (e.status == 0x2e.toByte() &&
connection.hasCtap1Support && connection.hasClientPin &&
options.signOptions.allowList.orEmpty().isNotEmpty() &&
options.signOptions.requireUserVerification != REQUIRED
) {
Log.d(TAG, "Falling back to CTAP1/U2F")
try {
ctap1sign(connection, options, clientDataHash)
} catch (e2: Exception) {
// Throw original exception
throw e
}
} else {
throw e
}
}
}
connection.hasCtap1Support -> ctap1sign(connection, options, clientDataHash)
else -> throw IllegalStateException()
}
return AuthenticatorAssertionResponse(
credentialId ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") },
clientData,
response.authData,
response.signature,
null
)
}
companion object {
const val TAG = "FidoTransportHandler"
}
}
interface TransportHandlerCallback {
fun onStatusChanged(transport: Transport, status: String, extras: Bundle? = null)
companion object {
@JvmStatic
val STATUS_WAITING_FOR_DEVICE = "waiting-for-device"
@JvmStatic
val STATUS_WAITING_FOR_USER = "waiting-for-user"
@JvmStatic
val STATUS_UNKNOWN = "unknown"
}
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.bluetooth
import android.bluetooth.BluetoothManager
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import androidx.core.content.getSystemService
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandler
import org.microg.gms.fido.core.transport.TransportHandlerCallback
class BluetoothTransportHandler(private val context: Context, callback: TransportHandlerCallback? = null) :
TransportHandler(Transport.BLUETOOTH, callback) {
override val isSupported: Boolean
get() = SDK_INT >= 18 && context.getSystemService<BluetoothManager>()?.adapter != null
}

View file

@ -0,0 +1,152 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.nfc
import android.content.Context
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.*
import org.microg.gms.utils.toBase64
class CtapNfcConnection(
val context: Context,
val tag: Tag
) : CtapConnection {
private val isoDep = IsoDep.get(tag)
override var capabilities: Int = 0
override var transports: List<String> = listOf("nfc")
override suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S {
require(hasCtap1Support)
Log.d(TAG, "Send CTAP1 command: ${command.request.apdu.toBase64(Base64.NO_WRAP)}")
val (statusCode, payload) = decodeResponseApdu(isoDep.transceive(command.request.apdu))
Log.d(TAG, "Received CTAP1 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
if (statusCode != 0x9000.toShort()) {
throw CtapNfcMessageStatusException(statusCode.toInt() and 0xffff)
}
return command.decodeResponse(statusCode, payload)
}
override suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S {
require(hasCtap2Support)
val request = encodeCommandApdu(0x80.toByte(), 0x10, 0x00, 0x00, byteArrayOf(command.request.commandByte) + command.request.payload, extended = true)
Log.d(TAG, "Send CTAP2 command: ${request.toBase64(Base64.NO_WRAP)} (${command.request.commandByte} - ${command.request.parameters})")
var (statusCode, payload) = decodeResponseApdu(isoDep.transceive(request))
Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
while (statusCode == 0x9100.toShort() || (statusCode > 0x6100.toShort() && statusCode < 0x6200.toShort())) {
Log.d(TAG, "Sending GETRESPONSE")
val res = decodeResponseApdu(isoDep.transceive(encodeCommandApdu(0x00, 0xC0.toByte(), 0x00,0x00)))
Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
if (statusCode < 0x6200.toShort()) {
payload = payload.plus(res.second)
} else {
payload = res.second
}
statusCode = res.first
}
if (statusCode != 0x9000.toShort()) {
throw CtapNfcMessageStatusException(statusCode.toInt() and 0xffff)
}
require(payload.isNotEmpty())
val ctapStatusCode = payload[0]
if (ctapStatusCode != 0x00.toByte()) {
throw Ctap2StatusException(ctapStatusCode)
}
return command.decodeResponse(payload, 1)
}
private fun select(aid: ByteArray): Pair<Short, ByteArray> {
Log.d(TAG, "Selecting AID: ${aid.toBase64(Base64.NO_WRAP)}")
return decodeResponseApdu(isoDep.transceive(encodeCommandApdu(0x00, 0xa4.toByte(), 0x04, 0x00, aid)))
}
private fun deselect() = isoDep.transceive(encodeCommandApdu(0x80.toByte(), 0x12, 0x01, 0x02))
private suspend fun fetchCapabilities() {
val response = runCommand(AuthenticatorGetInfoCommand())
Log.d(TAG, "Got info: $response")
capabilities = capabilities or CAPABILITY_CTAP_2 or
(if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0) or
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0) or
(if (response.options.userVerification == true) CAPABILITY_USER_VERIFICATION else 0) or
(if (response.options.residentKey == true) CAPABILITY_RESIDENT_KEY else 0)
if (response.transports != null) transports = response.transports
}
suspend fun open(): Boolean = withContext(Dispatchers.IO) {
isoDep.timeout = 5000
isoDep.connect()
val (statusCode, version) = select(FIDO2_AID)
if (statusCode == 0x9000.toShort()) {
Log.d(TAG, "Device sent version: ${version.decodeToString()}")
when (version.decodeToString()) {
"FIDO_2_0" -> {
capabilities = CAPABILITY_CTAP_2
try {
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
}
true
}
"U2F_V2" -> {
capabilities = CAPABILITY_CTAP_1 or CAPABILITY_CTAP_2
try {
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
capabilities = CAPABILITY_CTAP_1
}
true
}
else -> {
false
}
}
} else {
false
}
}
suspend fun close() = withContext(Dispatchers.IO) {
deselect()
isoDep.close()
capabilities = 0
}
suspend fun <R> open(block: suspend (CtapNfcConnection) -> R): R {
if (!open()) throw RuntimeException("Could not open device")
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when (exception) {
null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}
companion object {
const val TAG = "FidoCtapNfcConnection"
private val FIDO2_AID = byteArrayOf(0xA0.toByte(), 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01)
}
}
class CtapNfcMessageStatusException(val status: Int) : Exception("Received status ${status.toString(16)}")

View file

@ -0,0 +1,140 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.nfc
import android.app.Activity
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.core.app.OnNewIntentProvider
import androidx.core.app.PendingIntentCompat
import androidx.core.util.Consumer
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse
import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse
import com.google.android.gms.fido.fido2.api.common.RequestOptions
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import org.microg.gms.fido.core.MissingPinException
import org.microg.gms.fido.core.RequestOptionsType
import org.microg.gms.fido.core.WrongPinException
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandler
import org.microg.gms.fido.core.transport.TransportHandlerCallback
import org.microg.gms.fido.core.type
class NfcTransportHandler(private val activity: Activity, callback: TransportHandlerCallback? = null) :
TransportHandler(Transport.NFC, callback) {
override val isSupported: Boolean
get() = NfcAdapter.getDefaultAdapter(activity)?.isEnabled == true && activity is OnNewIntentProvider
private var deferred = CompletableDeferred<Tag>()
private suspend fun waitForNewNfcTag(adapter: NfcAdapter): Tag {
val intent = Intent(activity, activity.javaClass).apply { addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) }
val piOptions = if (SDK_INT >= 34) {
ActivityOptions.makeBasic().apply {
pendingIntentCreatorBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}.toBundle()
} else null
val pendingIntent: PendingIntent = PendingIntentCompat.getActivity(
activity, 0, intent,
0,
piOptions,
true)!!
adapter.enableForegroundDispatch(
activity,
pendingIntent,
arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)),
arrayOf(arrayOf(IsoDep::class.java.name))
)
invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE)
val tag = deferred.await()
deferred = CompletableDeferred()
return tag
}
suspend fun register(
options: RequestOptions,
callerPackage: String,
tag: Tag,
pinRequested: Boolean,
pin: String?
): AuthenticatorAttestationResponse {
return CtapNfcConnection(activity, tag).open {
register(it, activity, options, callerPackage, pinRequested, pin)
}
}
suspend fun sign(
options: RequestOptions,
callerPackage: String,
tag: Tag,
pinRequested: Boolean,
pin: String?
): AuthenticatorAssertionResponse {
return CtapNfcConnection(activity, tag).open {
sign(it, activity, options, callerPackage, pinRequested, pin)
}
}
suspend fun handle(
options: RequestOptions,
callerPackage: String,
tag: Tag,
pinRequested: Boolean,
pin: String?
): AuthenticatorResponse {
return when (options.type) {
RequestOptionsType.REGISTER -> register(options, callerPackage, tag, pinRequested, pin)
RequestOptionsType.SIGN -> sign(options, callerPackage, tag, pinRequested, pin)
}
}
override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse {
val adapter = NfcAdapter.getDefaultAdapter(activity)
val newIntentListener = Consumer<Intent> {
if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer
val tag = it.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return@Consumer
deferred.complete(tag)
}
try {
(activity as OnNewIntentProvider).addOnNewIntentListener(newIntentListener)
var ex: Exception? = null
for (i in 1..2) {
val tag = waitForNewNfcTag(adapter)
try {
return handle(options, callerPackage, tag, pinRequested, pin)
} catch (e: CancellationException) {
throw e
} catch (e: MissingPinException) {
throw e
} catch (e: WrongPinException) {
throw e
} catch (e: Exception) {
Log.w(TAG, e)
ex = e
}
}
throw ex ?: Exception("Unknown exception")
} finally {
(activity as OnNewIntentProvider).removeOnNewIntentListener(newIntentListener)
}
}
companion object {
const val TAG = "FidoNfcHandler"
}
}

View file

@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.screenlock
import android.content.Context
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.Build.VERSION.SDK_INT
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.security.keystore.StrongBoxUnavailableException
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import org.microg.gms.utils.toBase64
import java.security.*
import java.security.cert.Certificate
import java.security.spec.ECGenParameterSpec
import kotlin.random.Random
@RequiresApi(23)
class ScreenLockCredentialStore(val context: Context) {
private val keyStore by lazy { KeyStore.getInstance("AndroidKeyStore").apply { load(null) } }
private fun getAlias(rpId: String, keyId: ByteArray): String =
"1." + keyId.toBase64(Base64.NO_PADDING, Base64.NO_WRAP) + "." + rpId
private fun getPrivateKey(rpId: String, keyId: ByteArray) = keyStore.getKey(getAlias(rpId, keyId), null) as? PrivateKey
@RequiresApi(23)
fun createKey(rpId: String, challenge: ByteArray): ByteArray {
var useStrongbox = false
if (SDK_INT >= 28) useStrongbox = context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
val keyId = Random.nextBytes(32)
val identifier = getAlias(rpId, keyId)
Log.d(TAG, "Creating key for $identifier")
val generator = KeyPairGenerator.getInstance("EC", "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(identifier, KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setUserAuthenticationRequired(true)
if (SDK_INT >= 28) builder.setIsStrongBoxBacked(useStrongbox)
if (SDK_INT >= 24) builder.setAttestationChallenge(challenge)
var generatedKeypair = false
val exceptionClassesCaught = HashSet<Class<Exception>>()
while (!generatedKeypair) {
try {
generator.initialize(builder.build())
generator.generateKeyPair()
generatedKeypair = true
} catch (e: Exception) {
// Catch each exception class at most once.
// If we've caught the exception before, tried to correct it, and still catch the
// same exception, then we can't fix it and the exception should be thrown further
if (exceptionClassesCaught.contains(e.javaClass)) {
throw e
}
exceptionClassesCaught.add(e.javaClass)
if (SDK_INT >= 28 && e is StrongBoxUnavailableException) {
Log.w(TAG, "Failed with StrongBox, retrying without it...")
// Not all algorithms are backed by the Strongbox. If the Strongbox doesn't
// support this keypair, fall back to TEE
builder.setIsStrongBoxBacked(false)
} else if (SDK_INT >= 24 && e is ProviderException) {
Log.w(TAG, "Failed with attestation challenge, retrying without it...")
// This ProviderException is often thrown if the TEE or Strongbox doesn't have
// a built-in key to attest the new key pair with. If this happens, remove the
// attestation challenge and create an unattested key
builder.setAttestationChallenge(null)
} else {
// We don't know how to handle other errors, so they should be thrown up the
// system
throw e
}
}
}
return keyId
}
fun getPublicKey(rpId: String, keyId: ByteArray): PublicKey? =
keyStore.getCertificate(getAlias(rpId, keyId))?.publicKey
fun getCertificateChain(rpId: String, keyId: ByteArray): Array<Certificate> =
keyStore.getCertificateChain(getAlias(rpId, keyId))
fun getSignature(rpId: String, keyId: ByteArray): Signature? {
try {
val privateKey = getPrivateKey(rpId, keyId) ?: return null
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
return signature
} catch (e: KeyPermanentlyInvalidatedException) {
keyStore.deleteEntry(getAlias(rpId, keyId))
throw e
}
}
fun containsKey(rpId: String, keyId: ByteArray): Boolean = keyStore.containsAlias(getAlias(rpId, keyId))
companion object {
const val TAG = "FidoLockStore"
}
}

View file

@ -0,0 +1,281 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.screenlock
import android.app.KeyguardManager
import android.os.Build.VERSION.SDK_INT
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricPrompt
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.safetynet.SafetyNet
import com.google.android.gms.tasks.await
import kotlinx.coroutines.suspendCancellableCoroutine
import org.microg.gms.common.Constants
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.protocol.*
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandler
import org.microg.gms.fido.core.transport.TransportHandlerCallback
import org.microg.gms.utils.toBase64
import java.security.Signature
import java.security.interfaces.ECPublicKey
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@RequiresApi(23)
class ScreenLockTransportHandler(private val activity: FragmentActivity, callback: TransportHandlerCallback? = null) :
TransportHandler(Transport.SCREEN_LOCK, callback) {
private val store by lazy { ScreenLockCredentialStore(activity) }
private val database by lazy { Database(activity) }
override val isSupported: Boolean
get() = activity.getSystemService<KeyguardManager>()?.isDeviceSecure == true
suspend fun showBiometricPrompt(applicationName: String, signature: Signature?) {
suspendCancellableCoroutine<BiometricPrompt.AuthenticationResult> { continuation ->
val prompt = BiometricPrompt(activity, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
continuation.resume(result)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
val errorMessage = when (errorCode) {
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED, BiometricPrompt.ERROR_NEGATIVE_BUTTON -> "User canceled verification"
else -> errString.toString()
}
continuation.resumeWithException(RequestHandlingException(ErrorCode.NOT_ALLOWED_ERR, errorMessage))
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(R.string.fido_biometric_prompt_title))
.setDescription(
activity.getString(
R.string.fido_biometric_prompt_body,
applicationName
)
)
.setNegativeButtonText(activity.getString(android.R.string.cancel))
.build()
invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_USER)
if (signature != null) {
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature))
} else {
prompt.authenticate(promptInfo)
}
continuation.invokeOnCancellation { prompt.cancelAuthentication() }
}
}
suspend fun getActiveSignature(
options: RequestOptions,
callingPackage: String,
keyId: ByteArray
): Signature {
val signature =
store.getSignature(options.rpId, keyId) ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)
showBiometricPrompt(getApplicationName(activity, options, callingPackage), signature)
return signature
}
fun getCredentialData(aaguid: ByteArray, credentialId: CredentialId, coseKey: CoseKey) = AttestedCredentialData(
aaguid,
credentialId.encode(),
coseKey.encode()
)
fun getAuthenticatorData(
rpId: String,
credentialData: AttestedCredentialData?,
userPresent: Boolean = true,
userVerified: Boolean = true,
signCount: Int = 0
) = AuthenticatorData(
rpId.toByteArray().digest("SHA-256"),
userPresent = userPresent,
userVerified = userVerified,
signCount = signCount,
attestedCredentialData = credentialData
)
suspend fun register(
options: RequestOptions,
callerPackage: String
): AuthenticatorAttestationResponse {
if (options.type != RequestOptionsType.REGISTER) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)
val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId)
for (descriptor in options.registerOptions.excludeList.orEmpty()) {
val credentialBase64 = descriptor.id.toBase64(Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE)
val excluded = knownRegistrationInfo.any { it.credential == credentialBase64 }
if (store.containsKey(options.rpId, descriptor.id) || excluded) {
throw RequestHandlingException(
ErrorCode.NOT_ALLOWED_ERR,
"An excluded credential has already been registered with the device"
)
}
}
val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage)
val aaguid = if (options.registerOptions.skipAttestation) ByteArray(16) else AAGUID
val keyId = store.createKey(options.rpId, clientDataHash)
val publicKey =
store.getPublicKey(options.rpId, keyId) ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)
// We're ignoring the signature object as we don't need it for registration
val signature = getActiveSignature(options, callerPackage, keyId)
val (x, y) = (publicKey as ECPublicKey).w.let { it.affineX to it.affineY }
val coseKey = CoseKey(EC2Algorithm.ES256, x, y, 1, 32)
val credentialId = CredentialId(1, keyId, options.rpId, publicKey)
val credentialData = getCredentialData(aaguid, credentialId, coseKey)
val authenticatorData = getAuthenticatorData(options.rpId, credentialData)
val attestationObject = if (options.registerOptions.skipAttestation) {
NoneAttestationObject(authenticatorData)
} else {
try {
if (SDK_INT >= 24) {
createAndroidKeyAttestation(signature, authenticatorData, clientDataHash, options.rpId, keyId)
} else {
createSafetyNetAttestation(authenticatorData, clientDataHash)
}
} catch (e: Exception) {
Log.w("FidoScreenLockTransport", e)
NoneAttestationObject(authenticatorData)
}
}
return AuthenticatorAttestationResponse(
credentialId.encode(),
clientData,
attestationObject.encode(),
arrayOf("internal")
)
}
@RequiresApi(24)
private fun createAndroidKeyAttestation(
signature: Signature,
authenticatorData: AuthenticatorData,
clientDataHash: ByteArray,
rpId: String,
keyId: ByteArray
): AndroidKeyAttestationObject {
signature.update(authenticatorData.encode() + clientDataHash)
val sig = signature.sign()
return AndroidKeyAttestationObject(
authenticatorData,
EC2Algorithm.ES256,
sig,
store.getCertificateChain(rpId, keyId).map { it.encoded })
}
private suspend fun createSafetyNetAttestation(
authenticatorData: AuthenticatorData,
clientDataHash: ByteArray
): AndroidSafetyNetAttestationObject {
val response = SafetyNet.getClient(activity).attest(
(authenticatorData.encode() + clientDataHash).digest("SHA-256"),
"AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"
).await()
return AndroidSafetyNetAttestationObject(
authenticatorData,
Constants.GMS_VERSION_CODE.toString(),
response.jwsResult.toByteArray()
)
}
suspend fun sign(
options: RequestOptions,
callerPackage: String,
userInfo: String?
): AuthenticatorAssertionResponse {
if (options.type != RequestOptionsType.SIGN) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)
val candidates = mutableListOf<CredentialId>()
for (descriptor in options.signOptions.allowList.orEmpty()) {
try {
val (type, data) = CredentialId.decodeTypeAndData(descriptor.id)
if (type == 1.toByte() && store.containsKey(options.rpId, data)) {
candidates.add(CredentialId(type, data, options.rpId, store.getPublicKey(options.rpId, data)!!))
}
} catch (e: Exception) {
// Not in store or unknown id
}
}
val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId)
candidates.ifEmpty {
knownRegistrationInfo.mapNotNull {
val (type, data) = CredentialId.decodeTypeAndDataByBase64(it.credential)
if (type == 1.toByte() && store.containsKey(options.rpId, data)) {
CredentialId(type, data, options.rpId, store.getPublicKey(options.rpId, data)!!)
} else null
}.forEach {
candidates.add(it)
}
}
if (candidates.isEmpty()) {
// Show a biometric prompt even if no matching key to effectively rate-limit
showBiometricPrompt(getApplicationName(activity, options, callerPackage), null)
throw RequestHandlingException(
ErrorCode.NOT_ALLOWED_ERR,
"Cannot find credential in local KeyStore or database"
)
}
val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage)
val credentialUserInfo = if (userInfo != null) {
knownRegistrationInfo.firstOrNull { it.userJson == userInfo }
} else knownRegistrationInfo.firstOrNull()
val userHandle = credentialUserInfo?.let { PublicKeyCredentialUserEntity.parseJson(it.userJson).id }
val credentialId = candidates.firstOrNull { credentialUserInfo?.credential != null && credentialUserInfo.credential == it.toBase64() } ?: candidates.first()
val keyId = credentialId.data
val authenticatorData = getAuthenticatorData(options.rpId, null)
val signature = getActiveSignature(options, callerPackage, keyId)
signature.update(authenticatorData.encode() + clientDataHash)
val sig = signature.sign()
return AuthenticatorAssertionResponse(
credentialId.encode(),
clientData,
authenticatorData.encode(),
sig,
userHandle
)
}
@RequiresApi(24)
override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse =
when (options.type) {
RequestOptionsType.REGISTER -> register(options, callerPackage)
RequestOptionsType.SIGN -> sign(options, callerPackage, userInfo)
}
override fun shouldBeUsedInstantly(options: RequestOptions): Boolean {
if (options.type != RequestOptionsType.SIGN) return false
for (descriptor in options.signOptions.allowList.orEmpty()) {
try {
val (type, data) = CredentialId.decodeTypeAndData(descriptor.id)
if (type == 1.toByte() && store.containsKey(options.rpId, data)) {
return true
}
} catch (e: Exception) {
// Ignore
}
}
return false
}
companion object {
private val AAGUID = byteArrayOf(
0xb9.toByte(), 0x3f, 0xd9.toByte(), 0x61, 0xf2.toByte(), 0xe6.toByte(), 0x46, 0x2f,
0xb1.toByte(), 0x22, 0x82.toByte(), 0x00, 0x22, 0x47, 0xde.toByte(), 0x78
)
}
}

View file

@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import kotlinx.coroutines.CompletableDeferred
private val Context.usbPermissionCallbackAction
get() = "$packageName.USB_PERMISSION_CALLBACK"
private object UsbDevicePermissionReceiver : BroadcastReceiver() {
private var registered = false
private val pendingRequests = hashMapOf<UsbDevice, MutableList<CompletableDeferred<Boolean>>>()
fun register(context: Context) = synchronized(this) {
if (!registered) {
ContextCompat.registerReceiver(context, this, IntentFilter(context.usbPermissionCallbackAction), RECEIVER_NOT_EXPORTED)
registered = true
}
}
fun addDeferred(device: UsbDevice, deferred: CompletableDeferred<Boolean>) = synchronized(this) {
if (pendingRequests.containsKey(device)) {
pendingRequests[device]!!.add(deferred)
false
} else {
pendingRequests[device] = arrayListOf(deferred)
true
}
}
fun unregister(context: Context) = synchronized(this) {
if (registered) {
context.unregisterReceiver(this)
registered = false
}
}
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || context == null) return
if (intent.action == context.usbPermissionCallbackAction) {
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
if (device != null) {
synchronized(this) {
if (pendingRequests.containsKey(device)) {
val hasPermission = context.usbManager?.hasPermission(device) == true
for (deferred in pendingRequests[device].orEmpty()) {
deferred.complete(hasPermission)
}
pendingRequests.remove(device)
if (pendingRequests.isEmpty()) {
unregister(context)
}
}
}
}
}
}
}
class UsbDevicePermissionManager(private val context: Context) {
suspend fun awaitPermission(device: UsbDevice): Boolean {
if (context.usbManager?.hasPermission(device) == true) return true
val res = CompletableDeferred<Boolean>()
if (UsbDevicePermissionReceiver.addDeferred(device, res)) {
UsbDevicePermissionReceiver.register(context)
val intent = PendingIntentCompat.getBroadcast(context, 0, Intent(context.usbPermissionCallbackAction).apply { `package` = context.packageName }, 0, true)
context.usbManager?.requestPermission(device, intent)
}
return res.await()
}
}

View file

@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbConstants.*
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import com.google.android.gms.fido.fido2.api.common.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandler
import org.microg.gms.fido.core.transport.TransportHandlerCallback
import org.microg.gms.fido.core.transport.usb.ctaphid.CtapHidConnection
import org.microg.gms.utils.toBase64
@RequiresApi(21)
class UsbTransportHandler(private val context: Context, callback: TransportHandlerCallback? = null) :
TransportHandler(Transport.USB, callback) {
override val isSupported: Boolean
get() = context.packageManager.hasSystemFeature("android.hardware.usb.host") && context.usbManager != null
private val devicePermissionManager by lazy { UsbDevicePermissionManager(context) }
private var device: UsbDevice? = null
private infix fun <T> List<T>.eq(other: List<T>): Boolean =
other.size == size && zip(other).all { (x, y) -> x == y }
suspend fun getCtapHidInterface(device: UsbDevice): UsbInterface? {
for (iface in device.interfaces) {
if (iface.interfaceClass != USB_CLASS_HID) continue
if (iface.endpointCount != 2) continue
if (!iface.endpoints.all { it.type == USB_ENDPOINT_XFER_INT }) continue
if (!iface.endpoints.any { it.direction == USB_DIR_IN }) continue
if (!iface.endpoints.any { it.direction == USB_DIR_OUT }) continue
Log.d(TAG, "${device.productName} has suitable hid interface ${iface.id}")
if (!devicePermissionManager.awaitPermission(device)) continue
Log.d(TAG, "${device.productName} has permission")
val match = context.usbManager?.openDevice(device)?.use { connection ->
if (connection.claimInterface(iface, true)) {
val buf = ByteArray(256)
val read = connection.controlTransfer(0x81, 0x06, 0x2200, iface.id, buf, buf.size, 5000)
Log.d(TAG, "Signature: ${buf.slice(0 until read).toByteArray().toBase64(Base64.NO_WRAP)}")
read >= 5 && buf.slice(0 until 5) eq CTAPHID_SIGNATURE
} else {
Log.d(TAG, "Failed claiming interface")
false
}
} == true
if (match) {
return iface
} else {
Log.d(TAG, "${device.productName} signature does not match")
}
}
return null
}
suspend fun register(
options: RequestOptions,
callerPackage: String,
device: UsbDevice,
iface: UsbInterface,
pinRequested: Boolean,
pin: String?
): AuthenticatorAttestationResponse {
return CtapHidConnection(context, device, iface).open {
register(it, context, options, callerPackage, pinRequested, pin)
}
}
suspend fun sign(
options: RequestOptions,
callerPackage: String,
device: UsbDevice,
iface: UsbInterface,
pinRequested: Boolean,
pin: String?
): AuthenticatorAssertionResponse {
return CtapHidConnection(context, device, iface).open {
sign(it, context, options, callerPackage, pinRequested, pin)
}
}
private suspend fun waitForNewUsbDevice(): UsbDevice {
val deferred = CompletableDeferred<UsbDevice>()
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != UsbManager.ACTION_USB_DEVICE_ATTACHED) return
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
deferred.complete(device)
}
}
ContextCompat.registerReceiver(context, receiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), RECEIVER_NOT_EXPORTED)
invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE)
val device = deferred.await()
context.unregisterReceiver(receiver)
return device
}
suspend fun handle(
options: RequestOptions,
callerPackage: String,
device: UsbDevice,
iface: UsbInterface,
pinRequested: Boolean,
pin: String?
): AuthenticatorResponse {
Log.d(TAG, "Trying to use ${device.productName} for ${options.type}")
invokeStatusChanged(
TransportHandlerCallback.STATUS_WAITING_FOR_USER,
Bundle().apply { putParcelable(UsbManager.EXTRA_DEVICE, device) })
try {
return when (options.type) {
RequestOptionsType.REGISTER -> register(options, callerPackage, device, iface, pinRequested, pin)
RequestOptionsType.SIGN -> sign(options, callerPackage, device, iface, pinRequested, pin)
}
} finally {
this.device = null
}
}
override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse {
for (device in context.usbManager?.deviceList?.values.orEmpty()) {
val iface = getCtapHidInterface(device) ?: continue
try {
return handle(options, callerPackage, device, iface, pinRequested, pin)
} catch (e: CancellationException) {
throw e
} catch (e: MissingPinException) {
throw e
} catch (e: WrongPinException) {
throw e
} catch (e: Exception) {
Log.w(TAG, e)
}
}
// None of the already connected devices was suitable, waiting for new device
while (true) {
val device = waitForNewUsbDevice()
val iface = getCtapHidInterface(device) ?: continue
try {
return handle(options, callerPackage, device, iface, pinRequested, pin)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
companion object {
const val TAG = "FidoUsbHandler"
val CTAPHID_SIGNATURE = listOf<Byte>(0x06, 0xd0.toByte(), 0xf1.toByte(), 0x09, 0x01)
}
}

View file

@ -0,0 +1,201 @@
package org.microg.gms.fido.core.transport.usb.ctaphid
import android.content.Context
import android.hardware.usb.UsbConstants.USB_DIR_IN
import android.hardware.usb.UsbConstants.USB_DIR_OUT
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbRequest
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.*
import org.microg.gms.fido.core.transport.usb.endpoints
import org.microg.gms.fido.core.transport.usb.initialize
import org.microg.gms.fido.core.transport.usb.usbManager
import org.microg.gms.utils.toBase64
import java.nio.ByteBuffer
import kotlin.experimental.and
class CtapHidConnection(
val context: Context,
val device: UsbDevice,
val iface: UsbInterface,
) : CtapConnection {
private var connection: UsbDeviceConnection? = null
private val inEndpoint = iface.endpoints.first { it.direction == USB_DIR_IN }
private val outEndpoint = iface.endpoints.first { it.direction == USB_DIR_OUT }
private var channelIdentifier = 0xffffffff.toInt()
override var capabilities: Int = 0
override var transports: List<String> = listOf("usb")
suspend fun open(): Boolean {
Log.d(TAG, "Opening connection")
connection = context.usbManager?.openDevice(device)
if (connection?.claimInterface(iface, true) != true) {
Log.d(TAG, "Failed claiming interface")
close()
return false
}
val initRequest = CtapHidInitRequest()
sendRequest(initRequest)
val initResponse = readResponse()
if (initResponse !is CtapHidInitResponse || !initResponse.nonce.contentEquals(initRequest.nonce)) {
Log.d(TAG, "Failed init procedure")
close()
return false
}
channelIdentifier = initResponse.channelId
val caps = initResponse.capabilities
capabilities = 0 or
(if (caps and CtapHidInitResponse.CAPABILITY_NMSG == 0.toByte()) CAPABILITY_CTAP_1 else 0) or
(if (caps and CtapHidInitResponse.CAPABILITY_CBOR > 0) CAPABILITY_CTAP_2 else 0) or
(if (caps and CtapHidInitResponse.CAPABILITY_WINK > 0) CAPABILITY_WINK else 0)
if (hasCtap2Support) {
try {
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
}
}
return true
}
suspend fun close() {
connection?.close()
connection = null
channelIdentifier = 0xffffffff.toInt()
capabilities = 0
}
private suspend fun fetchCapabilities() {
val response = runCommand(AuthenticatorGetInfoCommand())
Log.d(TAG, "Got info: $response")
capabilities = capabilities or CAPABILITY_CTAP_2 or
(if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0) or
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0) or
(if (response.options.userVerification == true) CAPABILITY_USER_VERIFICATION else 0) or
(if (response.options.residentKey == true) CAPABILITY_RESIDENT_KEY else 0)
if (response.transports != null) transports = response.transports
}
suspend fun sendRequest(request: CtapHidRequest) {
val connection = connection ?: throw IllegalStateException("Not opened")
val packets = request.encodePackets(channelIdentifier, outEndpoint.maxPacketSize)
Log.d(TAG, "Sending $request in ${packets.size} packets")
UsbRequest().initialize(connection, outEndpoint) { outRequest ->
for (packet in packets) {
if (outRequest.queue(ByteBuffer.wrap(packet.bytes), packet.bytes.size)) {
withContext(Dispatchers.IO) { connection.requestWait() }
Log.d(TAG, "Sent packet ${packet.bytes.toBase64(Base64.NO_WRAP)}")
} else {
throw RuntimeException("Failed queuing packet")
}
}
}
}
suspend fun readResponse(timeout: Long = 1000): CtapHidResponse = withTimeout(timeout) {
val connection = connection ?: throw IllegalStateException("Not opened")
UsbRequest().initialize(connection, inEndpoint) { inRequest ->
val packets = mutableListOf<CtapHidPacket>()
val buffer = ByteBuffer.allocate(inEndpoint.maxPacketSize)
var initializationPacket: CtapHidInitializationPacket? = null
while (true) {
buffer.clear()
if (inRequest.queue(buffer, inEndpoint.maxPacketSize)) {
Log.d(TAG, "Reading ${inEndpoint.maxPacketSize} bytes from usb")
withContext(Dispatchers.IO) { connection.requestWait() }
Log.d(TAG, "Received packet ${buffer.array().toBase64(Base64.NO_WRAP)}")
if (initializationPacket == null) {
initializationPacket = CtapHidInitializationPacket.decode(buffer.array())
packets.add(initializationPacket)
} else {
val continuationPacket = CtapHidContinuationPacket.decode(buffer.array())
if (continuationPacket.channelIdentifier == initializationPacket.channelIdentifier) {
packets.add(continuationPacket)
} else {
Log.w(TAG, "Dropping unexpected packet: $continuationPacket")
}
}
if (packets.sumOf { it.data.size } >= initializationPacket.payloadLength) {
if (initializationPacket.channelIdentifier != channelIdentifier) {
packets.clear()
initializationPacket = null
} else {
val message = CtapHidMessage.decode(packets)
if (message.commandId == CtapHidKeepAliveMessage.COMMAND_ID) {
Log.w(TAG, "Keep alive: $message")
packets.clear()
initializationPacket = null
} else {
val response = CtapHidResponse.parse(message)
Log.d(TAG, "Received $response in ${packets.size} packets")
return@withTimeout response
}
}
}
} else {
throw RuntimeException("Failed queuing packet")
}
}
throw RuntimeException("Interrupted")
}
}
override suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S {
require(hasCtap1Support)
sendRequest(CtapHidMessageRequest(command.request))
val response = readResponse()
if (response is CtapHidMessageResponse) {
if (response.statusCode == 0x9000.toShort()) {
return command.decodeResponse(response.statusCode, response.payload)
}
throw CtapHidMessageStatusException(response.statusCode.toInt() and 0xffff)
}
throw RuntimeException("Unexpected response: $response")
}
override suspend fun <Q: Ctap2Request, S: Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S {
require(hasCtap2Support)
sendRequest(CtapHidCborRequest(command.request))
val response = readResponse(command.timeout)
if (response is CtapHidCborResponse) {
if (response.statusCode == 0x00.toByte()) {
return command.decodeResponse(response.payload)
}
throw Ctap2StatusException(response.statusCode)
}
throw RuntimeException("Unexpected response: $response")
}
suspend fun <R> open(block: suspend (CtapHidConnection) -> R): R {
if (!open()) throw RuntimeException("Could not open device")
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when (exception) {
null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}
companion object {
const val TAG = "FidoCtapHidConnection"
}
}
class CtapHidMessageStatusException(val status: Int) : Exception("Received status ${status.toString(16)}")

View file

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb.ctaphid
import android.util.Base64
import org.microg.gms.utils.toBase64
import kotlin.experimental.and
import kotlin.experimental.or
import kotlin.math.min
open class CtapHidMessage(val commandId: Byte, val data: ByteArray = ByteArray(0)) {
init {
if (data.size > Short.MAX_VALUE) throw IllegalArgumentException("Request too large")
}
fun encodePackets(channelIdentifier: Int, packetSize: Int): List<CtapHidPacket> {
val packets = arrayListOf<CtapHidPacket>()
val initializationDataSize = packetSize - 7
val continuationDataSize = packetSize - 5
var position = 0
var nextPosition = min(data.size, initializationDataSize)
packets.add(
CtapHidInitializationPacket(
channelIdentifier,
commandId or 0x80.toByte(),
data.size.toShort(),
data.sliceArray(position until nextPosition),
packetSize
)
)
var sequenceNumber: Byte = 0
while (nextPosition < data.size) {
position = nextPosition
nextPosition = min(data.size, position + continuationDataSize)
packets.add(
CtapHidContinuationPacket(
channelIdentifier,
sequenceNumber++,
data.sliceArray(position until nextPosition),
packetSize
)
)
}
return packets
}
override fun toString(): String =
"CtapHidMessage(commandId=0x${commandId.toString(16)}, data=${data.toBase64(Base64.NO_WRAP)})"
companion object {
fun decode(packets: List<CtapHidPacket>): CtapHidMessage {
val initializationPacket = packets.first() as? CtapHidInitializationPacket
?: throw IllegalArgumentException("First packet must ba an initialization packet")
val data = packets.map { it.data }.fold(ByteArray(0)) { a, b -> a + b }
.sliceArray(0 until initializationPacket.payloadLength)
return CtapHidMessage(initializationPacket.commandId and 0x7f, data)
}
}
}
class CtapHidKeepAliveMessage(val status: Byte) : CtapHidMessage(COMMAND_ID, byteArrayOf(status)) {
companion object {
const val STATUS_PROCESSING: Byte = 1
const val STATUS_UPNEEDED: Byte = 2
const val COMMAND_ID: Byte = 0x3b
}
}

View file

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb.ctaphid
import java.nio.ByteBuffer
import java.nio.ByteOrder
interface CtapHidPacket {
val bytes: ByteArray
val data: ByteArray
}
class CtapHidInitializationPacket(
val channelIdentifier: Int,
val commandId: Byte,
val payloadLength: Short,
override val data: ByteArray,
val packetSize: Int
) : CtapHidPacket {
override val bytes: ByteArray = ByteBuffer.allocate(packetSize).apply {
order(ByteOrder.BIG_ENDIAN)
position(0)
putInt(channelIdentifier)
put(commandId)
putShort(payloadLength)
put(data)
}.array()
init {
if (data.size > packetSize - 7) throw IllegalArgumentException("Too much data for packet size")
if (commandId >= 0) throw IllegalArgumentException("7-bit must be set on initialization packet")
}
companion object {
fun decode(bytes: ByteArray) = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).run {
CtapHidInitializationPacket(
int,
get(),
short,
ByteArray(bytes.size - 7).also { get(it) },
bytes.size
)
}
}
}
class CtapHidContinuationPacket(
val channelIdentifier: Int,
val sequenceNumber: Byte,
override val data: ByteArray,
val packetSize: Int
) : CtapHidPacket {
override val bytes: ByteArray = ByteBuffer.allocate(packetSize).apply {
order(ByteOrder.BIG_ENDIAN)
position(0)
putInt(channelIdentifier)
put(sequenceNumber)
put(data)
}.array()
init {
if (data.size > packetSize - 5) throw IllegalArgumentException("Too much data for packet size")
if (sequenceNumber < 0) throw IllegalArgumentException("7-bit must not be set on continuation packet")
}
companion object {
fun decode(bytes: ByteArray) = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).run {
CtapHidContinuationPacket(
int,
get(),
ByteArray(bytes.size - 5).also { get(it) },
bytes.size
)
}
}
}

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb.ctaphid
import android.util.Base64
import org.microg.gms.fido.core.protocol.msgs.Ctap1Request
import org.microg.gms.fido.core.protocol.msgs.Ctap2Request
import org.microg.gms.utils.toBase64
import kotlin.random.Random
abstract class CtapHidRequest(commandId: Byte, data: ByteArray = ByteArray(0)) : CtapHidMessage(commandId, data) {
override fun toString(): String =
"CtapHidRequest(commandId=0x${commandId.toString(16)}, data=${data.toBase64(Base64.NO_WRAP)})"
}
class CtapHidPingRequest(data: ByteArray) : CtapHidRequest(0x01, data) {
override fun toString(): String = "CtapHidPingRequest(data=${data.toBase64(Base64.NO_WRAP)})"
}
class CtapHidMessageRequest(val request: Ctap1Request) :
CtapHidRequest(0x03, request.apdu) {
override fun toString(): String = "CtapHidMessageRequest(${request})"
}
class CtapHidLockRequest(val seconds: Byte) : CtapHidRequest(0x04, byteArrayOf(seconds)) {
override fun toString(): String = "CtapHidLockRequest(seconds=$seconds)"
}
class CtapHidInitRequest(val nonce: ByteArray = Random.nextBytes(8)) :
CtapHidRequest(0x06, nonce) {
init {
if (nonce.size != 8) throw IllegalArgumentException("nonce must be 8 bytes")
}
override fun toString(): String = "CtapHidInitRequest(nonce=${nonce.toBase64(Base64.NO_WRAP)})"
}
class CtapHidWinkRequest : CtapHidRequest(0x08) {
override fun toString(): String = "CtapHidWinkRequest()"
}
class CtapHidCborRequest(val request: Ctap2Request) :
CtapHidRequest(0x10, byteArrayOf(request.commandByte) + request.payload) {
override fun toString(): String = "CtapHidCborRequest(${request})"
}
class CtapHidCancelRequest : CtapHidRequest(0x11) {
override fun toString(): String = "CtapHidCancelRequest()"
}

View file

@ -0,0 +1,164 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb.ctaphid
import android.util.Base64
import org.microg.gms.fido.core.protocol.msgs.decodeResponseApdu
import org.microg.gms.utils.toBase64
import java.nio.ByteBuffer
import java.nio.ByteOrder
open class CtapHidResponse(commandId: Byte, data: ByteArray) : CtapHidMessage(commandId, data) {
override fun toString(): String =
"CtapHidResponse(commandId=0x${commandId.toString(16)}, data=${data.toBase64(Base64.NO_WRAP)})"
companion object {
val responseTypes = mapOf(
CtapHidPingResponse.COMMAND_ID to CtapHidPingResponse::parse,
CtapHidMessageResponse.COMMAND_ID to CtapHidMessageResponse::parse,
CtapHidInitResponse.COMMAND_ID to CtapHidInitResponse::parse,
CtapHidWinkResponse.COMMAND_ID to CtapHidWinkResponse::parse,
CtapHidCborResponse.COMMAND_ID to CtapHidCborResponse::parse,
CtapHidErrorResponse.COMMAND_ID to CtapHidErrorResponse::parse,
)
fun parse(message: CtapHidMessage): CtapHidResponse =
responseTypes[message.commandId]?.invoke(message) ?: CtapHidResponse(message.commandId, message.data)
}
}
class CtapHidPingResponse(data: ByteArray) : CtapHidResponse(COMMAND_ID, data) {
override fun toString(): String = "CtapHidPingResponse(data=${data.toBase64(Base64.NO_WRAP)})"
companion object {
const val COMMAND_ID: Byte = 0x01
fun parse(message: CtapHidMessage): CtapHidPingResponse {
require(message.commandId == COMMAND_ID)
return CtapHidPingResponse(message.data)
}
}
}
class CtapHidMessageResponse(val statusCode: Short, val payload: ByteArray) :
CtapHidResponse(COMMAND_ID, byteArrayOf((statusCode.toInt() shr 8).toByte(), statusCode.toByte()) + payload) {
override fun toString(): String =
"CtapHidMessageResponse(statusCode=0x${statusCode.toString(16)}, payload=${payload.toBase64(Base64.NO_WRAP)})"
companion object {
const val COMMAND_ID: Byte = 0x03
fun parse(message: CtapHidMessage): CtapHidMessageResponse {
require(message.commandId == COMMAND_ID)
val (statusCode, payload) = decodeResponseApdu(message.data)
return CtapHidMessageResponse(statusCode, payload)
}
}
}
class CtapHidInitResponse(
val nonce: ByteArray,
val channelId: Int,
val protocolVersion: Byte,
val majorDeviceVersion: Byte,
val minorDeviceVersion: Byte,
val buildDeviceVersion: Byte,
val capabilities: Byte
) : CtapHidResponse(COMMAND_ID, ByteBuffer.allocate(17).apply {
order(ByteOrder.BIG_ENDIAN)
position(0)
put(nonce)
putInt(channelId)
put(protocolVersion)
put(majorDeviceVersion)
put(minorDeviceVersion)
put(buildDeviceVersion)
put(capabilities)
}.array()) {
val deviceVersion: String = "$majorDeviceVersion.$minorDeviceVersion.$buildDeviceVersion"
override fun toString(): String = "CtapHidInitResponse(nonce=0x${nonce.toBase64(Base64.NO_WRAP)}, " +
"channelId=0x${channelId.toString(16)}, " +
"protocolVersion=0x${protocolVersion.toString(16)}, " +
"version=$deviceVersion, " +
"capabilities=0x${capabilities.toString(16)})"
companion object {
const val COMMAND_ID: Byte = 0x06
const val CAPABILITY_WINK: Byte = 0x01
const val CAPABILITY_CBOR: Byte = 0x04
const val CAPABILITY_NMSG: Byte = 0x08
fun parse(message: CtapHidMessage): CtapHidInitResponse {
require(message.commandId == COMMAND_ID)
require(message.data.size == 17)
return ByteBuffer.wrap(message.data).order(ByteOrder.BIG_ENDIAN).run {
CtapHidInitResponse(
ByteArray(8).also { get(it) },
int,
get(),
get(),
get(),
get(),
get()
)
}
}
}
}
class CtapHidWinkResponse : CtapHidResponse(COMMAND_ID, ByteArray(0)) {
override fun toString(): String = "CtapHidWinkResponse()"
companion object {
const val COMMAND_ID: Byte = 0x08
fun parse(message: CtapHidMessage): CtapHidWinkResponse {
require(message.commandId == COMMAND_ID)
require(message.data.isEmpty())
return CtapHidWinkResponse()
}
}
}
class CtapHidCborResponse(val statusCode: Byte, val payload: ByteArray) :
CtapHidResponse(COMMAND_ID, byteArrayOf(statusCode) + payload) {
override fun toString(): String =
"CtapHidCborResponse(statusCode=0x${statusCode.toString(16)}, payload=${payload.toBase64(Base64.NO_WRAP)})"
companion object {
const val COMMAND_ID: Byte = 0x10
fun parse(message: CtapHidMessage): CtapHidCborResponse {
require(message.commandId == COMMAND_ID)
return CtapHidCborResponse(message.data[0], message.data.sliceArray(1 until message.data.size))
}
}
}
class CtapHidErrorResponse(val errorCode: Byte) : CtapHidResponse(COMMAND_ID, byteArrayOf(errorCode)) {
override fun toString(): String = "CtapHidErrorResponse(errorCode=0x${errorCode.toString(16)})"
companion object {
const val COMMAND_ID: Byte = 0x3f
const val ERR_INVALID_CMD: Byte = 0x01
const val ERR_INVALID_PAR: Byte = 0x02
const val ERR_INVALID_LEN: Byte = 0x03
const val ERR_INVALID_SEQ: Byte = 0x04
const val ERR_MSG_TIMEOUT: Byte = 0x05
const val ERR_CHANNEL_BUSY: Byte = 0x06
const val ERR_LOCK_REQUIRED: Byte = 0x0A
const val ERR_INVALID_CHANNEL: Byte = 0x0B
const val ERR_OTHER: Byte = 0x7F
fun parse(message: CtapHidMessage): CtapHidErrorResponse {
require(message.commandId == COMMAND_ID)
require(message.data.size == 1)
return CtapHidErrorResponse(message.data[0])
}
}
}

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.transport.usb
import android.content.Context
import android.hardware.usb.*
val Context.usbManager: UsbManager?
get() = getSystemService(Context.USB_SERVICE) as? UsbManager?
val UsbDevice.interfaces: Iterable<UsbInterface>
get() = Iterable {
object : Iterator<UsbInterface> {
private var index = 0
override fun hasNext(): Boolean = index < interfaceCount
override fun next(): UsbInterface = getInterface(index++)
}
}
val UsbInterface.endpoints: Iterable<UsbEndpoint>
get() = Iterable {
object : Iterator<UsbEndpoint> {
private var index = 0
override fun hasNext(): Boolean = index < endpointCount
override fun next(): UsbEndpoint = getEndpoint(index++)
}
}
inline fun <R> UsbDeviceConnection.use(block: (UsbDeviceConnection) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when (exception) {
null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}
inline fun <R> UsbRequest.initialize(connection: UsbDeviceConnection, endpoint: UsbEndpoint, block: (UsbRequest) -> R): R {
var exception: Throwable? = null
try {
initialize(connection, endpoint)
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when (exception) {
null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}

View file

@ -0,0 +1,365 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.util.Base64
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
import com.google.android.gms.fido.Fido.*
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsPrfOutputs
import com.google.android.gms.fido.fido2.api.common.ErrorCode.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import org.microg.gms.common.GmsService
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.Transport.*
import org.microg.gms.fido.core.transport.TransportHandler
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.getApplicationLabel
import org.microg.gms.utils.getFirstSignatureDigest
import org.microg.gms.utils.toBase64
const val TAG = "FidoUi"
class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {
val options: RequestOptions?
get() = when (intent.getStringExtra(KEY_SOURCE) to intent.getStringExtra(KEY_TYPE)) {
SOURCE_BROWSER to TYPE_REGISTER ->
BrowserPublicKeyCredentialCreationOptions.deserializeFromBytes(intent.getByteArrayExtra(KEY_OPTIONS))
SOURCE_BROWSER to TYPE_SIGN ->
BrowserPublicKeyCredentialRequestOptions.deserializeFromBytes(intent.getByteArrayExtra(KEY_OPTIONS))
SOURCE_APP to TYPE_REGISTER ->
PublicKeyCredentialCreationOptions.deserializeFromBytes(intent.getByteArrayExtra(KEY_OPTIONS))
SOURCE_APP to TYPE_SIGN ->
PublicKeyCredentialRequestOptions.deserializeFromBytes(intent.getByteArrayExtra(KEY_OPTIONS))
else -> null
}
private val service: GmsService
get() = GmsService.byServiceId(intent.getIntExtra(KEY_SERVICE, GmsService.UNKNOWN.SERVICE_ID))
private val database by lazy { Database(this) }
private val transportHandlers by lazy {
setOfNotNull(
BluetoothTransportHandler(this, this),
NfcTransportHandler(this, this),
if (SDK_INT >= 21) UsbTransportHandler(this, this) else null,
if (SDK_INT >= 23) ScreenLockTransportHandler(this, this) else null
)
}
lateinit var callerPackage: String
lateinit var callerSignature: String
private lateinit var navHostFragment: NavHostFragment
private inline fun <reified T : TransportHandler> getTransportHandler(): T? =
transportHandlers.filterIsInstance<T>().firstOrNull { it.isSupported }
fun getTransportHandler(transport: Transport): TransportHandler? =
transportHandlers.firstOrNull { it.transport == transport && it.isSupported }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
val callerPackage = (if (callingActivity?.packageName == packageName && intent.hasExtra(KEY_CALLER)) intent.getStringExtra(KEY_CALLER) else callingActivity?.packageName) ?: return finish()
if (!intent.extras?.keySet().orEmpty().containsAll(REQUIRED_EXTRAS)) {
return finishWithError(UNKNOWN_ERR, "Extra missing from request")
}
if (SDK_INT < 24) {
return finishWithError(NOT_SUPPORTED_ERR, "FIDO2 API is not supported on devices below N")
}
val options = options ?: return finishWithError(DATA_ERR, "The request options are not valid")
this.callerPackage = callerPackage
this.callerSignature = packageManager.getFirstSignatureDigest(callerPackage, "SHA-256")?.toBase64()
?: return finishWithError(UNKNOWN_ERR, "Could not determine signature of app")
Log.d(TAG, "onCreate caller=$callerPackage options=$options")
val requiresPrivilege =
options is BrowserRequestOptions && !database.isPrivileged(callerPackage, callerSignature)
// Check if we can directly open screen lock handling
if (!requiresPrivilege) {
val instantTransport = transportHandlers.firstOrNull { it.isSupported && it.shouldBeUsedInstantly(options) }
if (instantTransport != null && instantTransport.transport in INSTANT_SUPPORTED_TRANSPORTS) {
window.setBackgroundDrawable(ColorDrawable(0))
window.statusBarColor = Color.TRANSPARENT
setTheme(org.microg.gms.base.core.R.style.Theme_Translucent)
}
}
setTheme(androidx.appcompat.R.style.Theme_AppCompat_DayNight_NoActionBar)
setContentView(R.layout.fido_authenticator_activity)
lifecycleScope.launchWhenCreated {
handleRequest(options)
}
} catch (e: RequestHandlingException) {
finishWithError(e.errorCode, e.message ?: e.errorCode.name)
} catch (e: Exception) {
Log.w(TAG, e)
finishWithError(UNKNOWN_ERR, e.message ?: e.javaClass.simpleName)
}
}
@RequiresApi(24)
suspend fun handleRequest(options: RequestOptions, allowInstant: Boolean = true) {
try {
val origin = getOrigin(this, options, callerPackage)
options.checkIsValid(this, origin, callerPackage)
val appName = getApplicationName(this, options, callerPackage)
val callerName = packageManager.getApplicationLabel(callerPackage).toString()
val requiresPrivilege =
options is BrowserRequestOptions && !database.isPrivileged(callerPackage, callerSignature)
Log.d(TAG, "origin=$origin, appName=$appName")
// Check if we can directly open screen lock handling
if (!requiresPrivilege && allowInstant) {
val instantTransport = transportHandlers.firstOrNull { it.isSupported && it.shouldBeUsedInstantly(options) }
if (instantTransport != null && instantTransport.transport in INSTANT_SUPPORTED_TRANSPORTS) {
startTransportHandling(instantTransport.transport, true)
return
}
}
val arguments = AuthenticatorActivityFragmentData().apply {
this.appName = appName
this.isFirst = true
this.privilegedCallerName = callerName.takeIf { options is BrowserRequestOptions }
this.requiresPrivilege = requiresPrivilege
this.supportedTransports = transportHandlers.filter { it.isSupported }.map { it.transport }.toSet()
}.arguments
val next = if (!requiresPrivilege) {
val knownRegistrationTransports = mutableSetOf<Transport>()
val allowedTransports = mutableSetOf<Transport>()
val localSavedUserKey = mutableSetOf<String>()
if (options.type == RequestOptionsType.SIGN) {
for (descriptor in options.signOptions.allowList.orEmpty()) {
val knownTransport = database.getKnownRegistrationTransport(options.rpId, descriptor.id.toBase64(Base64.URL_SAFE, Base64.NO_WRAP, Base64.NO_PADDING))
if (knownTransport != null && knownTransport in IMPLEMENTED_TRANSPORTS)
knownRegistrationTransports.add(knownTransport)
if (descriptor.transports.isNullOrEmpty()) {
allowedTransports.addAll(Transport.values())
} else {
for (transport in descriptor.transports.orEmpty()) {
val allowedTransport = when (transport) {
com.google.android.gms.fido.common.Transport.BLUETOOTH_CLASSIC -> BLUETOOTH
com.google.android.gms.fido.common.Transport.BLUETOOTH_LOW_ENERGY -> BLUETOOTH
com.google.android.gms.fido.common.Transport.NFC -> NFC
com.google.android.gms.fido.common.Transport.USB -> USB
com.google.android.gms.fido.common.Transport.INTERNAL -> SCREEN_LOCK
else -> null
}
if (allowedTransport != null && allowedTransport in IMPLEMENTED_TRANSPORTS)
allowedTransports.add(allowedTransport)
}
}
}
database.getKnownRegistrationInfo(options.rpId).forEach { localSavedUserKey.add(it.userJson) }
}
val preselectedTransport = knownRegistrationTransports.singleOrNull() ?: allowedTransports.singleOrNull()
if (database.wasUsed()) {
if (localSavedUserKey.isNotEmpty()) {
R.id.signInSelectionFragment
} else when (preselectedTransport) {
USB -> R.id.usbFragment
NFC -> R.id.nfcFragment
else -> R.id.transportSelectionFragment
}
} else {
null
}
} else {
null
}
navHostFragment = NavHostFragment()
supportFragmentManager.commit {
replace(R.id.fragment_container, navHostFragment)
runOnCommit {
val navGraph = navHostFragment.navController.navInflater.inflate(R.navigation.nav_fido_authenticator)
if (next != null) {
navGraph.setStartDestination(next)
}
navHostFragment.navController.setGraph(navGraph, arguments)
}
}
} catch (e: RequestHandlingException) {
finishWithError(e.errorCode, e.message ?: e.errorCode.name)
} catch (e: Exception) {
Log.w(TAG, e)
finishWithError(UNKNOWN_ERR, e.message ?: e.javaClass.simpleName)
}
}
fun finishWithError(errorCode: ErrorCode, errorMessage: String) {
Log.d(TAG, "Finish with error: $errorMessage ($errorCode)")
finishWithCredential(
PublicKeyCredential.Builder().setResponse(AuthenticatorErrorResponse(errorCode, errorMessage)).build()
)
}
fun finishWithSuccessResponse(response: AuthenticatorResponse, transport: Transport) {
Log.d(TAG, "Finish with success response: $response")
if (options is BrowserRequestOptions) database.insertPrivileged(callerPackage, callerSignature)
val rpId = options?.rpId
val rawId = when(response) {
is AuthenticatorAttestationResponse -> response.keyHandle
is AuthenticatorAssertionResponse -> response.keyHandle
else -> null
}
val id = rawId?.toBase64(Base64.URL_SAFE, Base64.NO_WRAP, Base64.NO_PADDING)
if (rpId != null && id != null) {
database.insertKnownRegistration(rpId, id, transport, options?.user)
}
val prfFirst = rawId?.let { java.security.MessageDigest.getInstance("SHA-256").digest(it) }?.copyOf(32)
val prfOutputs = prfFirst?.let { AuthenticationExtensionsPrfOutputs(true, it, null) }
val clientExtResults = AuthenticationExtensionsClientOutputs(
null,
null,
AuthenticationExtensionsCredPropsOutputs(true),
prfOutputs,
null
)
val pkc = PublicKeyCredential.Builder()
.setResponse(response)
.setRawId(rawId ?: ByteArray(0).also { Log.w(TAG, "rawId was null") })
.setId(id ?: "".also { Log.w(TAG, "id was null") })
.setAuthenticatorAttachment(if (transport == SCREEN_LOCK) "platform" else "cross-platform")
.setAuthenticationExtensionsClientOutputs(clientExtResults)
.build()
finishWithCredential(pkc)
}
private fun finishWithCredential(publicKeyCredential: PublicKeyCredential) {
val intent = Intent()
intent.putExtra(FIDO2_KEY_CREDENTIAL_EXTRA, publicKeyCredential.serializeToBytes())
val response: AuthenticatorResponse = publicKeyCredential.response
if (response is AuthenticatorErrorResponse) {
intent.putExtra(FIDO2_KEY_ERROR_EXTRA, response.serializeToBytes())
} else {
intent.putExtra(FIDO2_KEY_RESPONSE_EXTRA, response.serializeToBytes())
}
setResult(RESULT_OK, intent)
finish()
}
fun shouldStartTransportInstantly(transport: Transport): Boolean {
return getTransportHandler(transport)?.shouldBeUsedInstantly(options ?: return false) == true
}
fun isScreenLockSigner(): Boolean {
return shouldStartTransportInstantly(SCREEN_LOCK)
}
@RequiresApi(24)
fun startTransportHandling(transport: Transport, instant: Boolean = false, pinRequested: Boolean = false, authenticatorPin: String? = null, userInfo: String? = null): Job = lifecycleScope.launchWhenResumed {
val options = options ?: return@launchWhenResumed
try {
finishWithSuccessResponse(getTransportHandler(transport)!!.start(options, callerPackage, pinRequested, authenticatorPin, userInfo), transport)
} catch (e: SecurityException) {
Log.w(TAG, e)
if (instant) {
handleRequest(options, false)
} else {
finishWithError(SECURITY_ERR, e.message ?: e.javaClass.simpleName)
}
} catch (e: CancellationException) {
Log.w(TAG, e)
// Ignoring cancellation here
} catch (e: RequestHandlingException) {
Log.w(TAG, e)
finishWithError(e.errorCode, e.message ?: e.errorCode.name)
} catch (e: MissingPinException) {
// Redirect the user to ask for a PIN code
navHostFragment.navController.navigate(R.id.openPinFragment)
} catch (e: WrongPinException) {
// Redirect the user, and inform them that the pin was wrong
Toast.makeText(baseContext, R.string.fido_wrong_pin, Toast.LENGTH_LONG).show()
navHostFragment.navController.navigate(R.id.openPinFragment)
} catch (e: Exception) {
Log.w(TAG, e)
finishWithError(UNKNOWN_ERR, e.message ?: e.javaClass.simpleName)
}
}
override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) {
Log.d(TAG, "$transport status set to $status ($extras)")
try {
for (callback in navHostFragment.childFragmentManager.fragments.filterIsInstance<TransportHandlerCallback>()) {
try {
callback.onStatusChanged(transport, status, extras)
} catch (e: Exception) {
// Ignoring
}
}
} catch (e: Exception) {
// Ignoring
}
}
override fun onBackPressed() {
try {
if (navHostFragment.navController.popBackStack()) return
} catch (e: Exception) {
// Ignore
}
super.onBackPressed()
}
override fun onSupportNavigateUp(): Boolean {
try {
if (navHostFragment.navController.navigateUp()) return true
} catch (e: Exception) {
// Ignore
}
return super.onSupportNavigateUp()
}
companion object {
const val KEY_SERVICE = "service"
const val KEY_SOURCE = "source"
const val KEY_TYPE = "type"
const val KEY_OPTIONS = "options"
val REQUIRED_EXTRAS = setOf(KEY_SERVICE, KEY_SOURCE, KEY_TYPE, KEY_OPTIONS)
const val SOURCE_BROWSER = "browser"
const val SOURCE_APP = "app"
const val TYPE_REGISTER = "register"
const val TYPE_SIGN = "sign"
const val KEY_CALLER = "caller"
val IMPLEMENTED_TRANSPORTS = setOf(USB, SCREEN_LOCK, NFC)
val INSTANT_SUPPORTED_TRANSPORTS = setOf(SCREEN_LOCK)
}
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.annotation.TargetApi
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.fido.fido2.api.common.ErrorCode
import com.google.android.gms.fido.fido2.api.common.RequestOptions
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.transport.Transport
@TargetApi(24)
abstract class AuthenticatorActivityFragment : Fragment() {
private val pinViewModel: AuthenticatorPinViewModel by activityViewModels()
val data: AuthenticatorActivityFragmentData
get() = AuthenticatorActivityFragmentData(arguments ?: Bundle.EMPTY)
val authenticatorActivity: AuthenticatorActivity?
get() = activity as? AuthenticatorActivity
val options: RequestOptions?
get() = authenticatorActivity?.options
fun startTransportHandling(transport: Transport, userInfo: String? = null) =
authenticatorActivity?.startTransportHandling(transport, pinRequested = pinViewModel.pinRequest, authenticatorPin = pinViewModel.pin, userInfo = userInfo)
fun shouldStartTransportInstantly(transport: Transport) = authenticatorActivity?.shouldStartTransportInstantly(transport) == true
abstract override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.os.Bundle
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData.Companion.KEY_IS_FIRST
class AuthenticatorActivityFragmentData(val arguments: Bundle = Bundle()) {
var appName: String?
get() = arguments.getString(KEY_APP_NAME)
set(value) = arguments.putString(KEY_APP_NAME, value)
var isFirst: Boolean
get() = arguments.getBoolean(KEY_IS_FIRST, true)
set(value) = arguments.putBoolean(KEY_IS_FIRST, value)
var supportedTransports: Set<Transport>
get() = arguments.getStringArrayList(KEY_SUPPORTED_TRANSPORTS)?.map { Transport.valueOf(it) }?.toSet().orEmpty()
set(value) = arguments.putStringArrayList(KEY_SUPPORTED_TRANSPORTS, ArrayList(value.map { it.name }))
val implementedTransports: Set<Transport>
get() = AuthenticatorActivity.IMPLEMENTED_TRANSPORTS
var privilegedCallerName: String?
get() = arguments.getString(KEY_PRIVILEGED_CALLER_NAME)
set(value) = arguments.putString(KEY_PRIVILEGED_CALLER_NAME, value)
var requiresPrivilege: Boolean
get() = arguments.getBoolean(KEY_REQUIRES_PRIVILEGE)
set(value) = arguments.putBoolean(KEY_REQUIRES_PRIVILEGE, value)
companion object {
const val KEY_APP_NAME = "appName"
const val KEY_IS_FIRST = "isFirst"
const val KEY_SUPPORTED_TRANSPORTS = "supportedTransports"
const val KEY_REQUIRES_PRIVILEGE = "requiresPrivilege"
const val KEY_PRIVILEGED_CALLER_NAME = "privilegedCallerName"
}
}
fun Bundle?.withIsFirst(isFirst: Boolean) = Bundle(this ?: Bundle.EMPTY).apply { putBoolean(KEY_IS_FIRST, isFirst) }

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.databinding.FidoNfcTransportFragmentBinding
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandlerCallback
class NfcTransportFragment : AuthenticatorActivityFragment(), TransportHandlerCallback {
private lateinit var binding: FidoNfcTransportFragmentBinding
private var job: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FidoNfcTransportFragmentBinding.inflate(inflater, container, false)
binding.data = data
binding.onBackClick = View.OnClickListener {
if (!findNavController().navigateUp()) {
findNavController().navigate(
R.id.transportSelectionFragment,
arguments,
navOptions { popUpTo(R.id.usbFragment) { inclusive = true } })
}
}
if (SDK_INT >= 23) {
(binding.fidoNfcWaitConnectAnimation.drawable as? AnimatedVectorDrawable)?.registerAnimationCallback(object :
Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
lifecycleScope.launchWhenStarted {
delay(250)
(drawable as? AnimatedVectorDrawable)?.reset()
delay(500)
(drawable as? AnimatedVectorDrawable)?.start()
}
}
})
(binding.fidoNfcWaitConnectAnimation.drawable as? AnimatedVectorDrawable)?.start()
}
return binding.root
}
override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) {
if (transport != Transport.NFC) return
binding.status = status
}
override fun onStart() {
super.onStart()
job = startTransportHandling(Transport.NFC)
}
override fun onStop() {
job?.cancel()
super.onStop()
}
}

View file

@ -0,0 +1,83 @@
package org.microg.gms.fido.core.ui
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import androidx.core.content.getSystemService
import androidx.databinding.adapters.TextViewBindingAdapter
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModel
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.databinding.FidoPinFragmentBinding
class AuthenticatorPinViewModel : ViewModel() {
var pinRequest: Boolean = false
var pin: String? = null
}
class PinFragment: AuthenticatorActivityFragment() {
private lateinit var binding: FidoPinFragmentBinding
private val pinViewModel: AuthenticatorPinViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FidoPinFragmentBinding.inflate(inflater, container, false)
binding.onCancel = View.OnClickListener {
leaveFragment()
}
binding.onEnterPin = View.OnClickListener {
enterPin()
}
binding.onInputChange = TextViewBindingAdapter.AfterTextChanged {
view?.findViewById<Button>(R.id.pin_fragment_ok)?.isEnabled = it.toString().encodeToByteArray().size in 4..63
}
binding.root.findViewById<EditText>(R.id.pin_editor)?.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE &&
(event == null || event.action == KeyEvent.ACTION_DOWN) &&
v.text.toString().encodeToByteArray().size in 4 ..63) {
enterPin()
true
} else {
false
}
}
return binding.root
}
override fun onResume() {
super.onResume()
view?.findViewById<EditText>(R.id.pin_editor)?.let { editText ->
requireContext().getSystemService<InputMethodManager>()?.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
}
fun enterPin () {
val textEditor = view?.findViewById<EditText>(R.id.pin_editor)
if (textEditor != null) {
pinViewModel.pin = textEditor.text.toString()
}
leaveFragment()
}
fun leaveFragment() {
pinViewModel.pinRequest = true
if (!findNavController().navigateUp()) {
findNavController().navigate(
R.id.transportSelectionFragment,
arguments,
navOptions { popUpTo(R.id.usbFragment) { inclusive = true } })
}
}
}

View file

@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.gms.common.images.ImageManager
import com.google.android.gms.fido.fido2.api.common.ErrorCode
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
import org.microg.gms.fido.core.CredentialUserInfo
import org.microg.gms.fido.core.Database
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.databinding.FidoSignInSelectionFragmentBinding
import org.microg.gms.fido.core.rpId
import org.microg.gms.fido.core.transport.Transport
import androidx.core.view.isGone
class SignInSelectionFragment : AuthenticatorActivityFragment() {
private lateinit var binding: FidoSignInSelectionFragmentBinding
private val database by lazy { Database(requireContext()) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FidoSignInSelectionFragmentBinding.inflate(inflater, container, false)
binding.data = data
binding.signInKeyRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.signInKeyBack.setOnClickListener { requireActivity().finish() }
return binding.root.apply { isGone }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val rpId = options?.rpId
if (rpId.isNullOrEmpty()) {
authenticatorActivity?.finishWithError(ErrorCode.UNKNOWN_ERR, "Missing rpId")
return
}
val knownRegistrationInfo = database.getKnownRegistrationInfo(rpId)
if (knownRegistrationInfo.isEmpty()) {
findNavController().navigate(R.id.openWelcomeFragment)
} else if (knownRegistrationInfo.size == 1) {
val info = knownRegistrationInfo.first()
startTransportHandling(info.transport, info.userJson)
} else {
binding.root.apply { isVisible }
binding.signInKeyRecycler.adapter = SignInKeyAdapter(knownRegistrationInfo) { user, transport ->
startTransportHandling(transport, user)
}
}
}
}
internal class SignInKeyAdapter(val data: List<CredentialUserInfo>, val onKeyClick: (String, Transport) -> Unit) :
RecyclerView.Adapter<SignInKeyAdapter.SignInHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SignInHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fido_sign_in_item_layout, parent, false)
return SignInHolder(view)
}
override fun onBindViewHolder(holder: SignInHolder, position: Int) {
val item = data[position]
val user = PublicKeyCredentialUserEntity.parseJson(item.userJson)
holder.signInKeyName.text = user.displayName
holder.signInKeyEmail.text = user.name
user.icon?.let { ImageManager.create(holder.itemView.context).loadImage(it, holder.signInKeyLogo) }
holder.itemView.setOnClickListener { onKeyClick(item.userJson, item.transport) }
}
override fun getItemCount(): Int {
return data.size
}
inner class SignInHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val signInKeyLogo: ImageView = itemView.findViewById(R.id.sign_in_key_logo)
val signInKeyName: TextView = itemView.findViewById(R.id.sign_in_key_name)
val signInKeyEmail: TextView = itemView.findViewById(R.id.sign_in_key_email)
}
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.databinding.FidoTransportSelectionFragmentBinding
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.Transport.SCREEN_LOCK
class TransportSelectionFragment : AuthenticatorActivityFragment() {
private lateinit var binding: FidoTransportSelectionFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FidoTransportSelectionFragmentBinding.inflate(inflater, container, false)
binding.data = data
binding.setOnBluetoothClick {
findNavController().navigate(R.id.openBluetoothFragment, arguments.withIsFirst(false))
}
binding.setOnNfcClick {
findNavController().navigate(R.id.openNfcFragment, arguments.withIsFirst(false))
}
binding.setOnUsbClick {
findNavController().navigate(R.id.openUsbFragment, arguments.withIsFirst(false))
}
binding.setOnScreenLockClick {
startTransportHandling(SCREEN_LOCK)
}
return binding.root
}
}

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavOptions
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.databinding.FidoUsbTransportFragmentBinding
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.transport.TransportHandlerCallback
class UsbTransportFragment : AuthenticatorActivityFragment(), TransportHandlerCallback {
private lateinit var binding: FidoUsbTransportFragmentBinding
private var job: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FidoUsbTransportFragmentBinding.inflate(inflater, container, false)
binding.data = data
binding.onBackClick = View.OnClickListener {
if (!findNavController().navigateUp()) {
findNavController().navigate(
R.id.transportSelectionFragment,
arguments,
navOptions { popUpTo(R.id.usbFragment) { inclusive = true } })
}
}
if (SDK_INT >= 23) {
for (imageView in listOfNotNull(binding.fidoUsbWaitConnectAnimation, binding.fidoUsbWaitConfirmAnimation)) {
(imageView.drawable as? AnimatedVectorDrawable)?.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
lifecycleScope.launchWhenStarted {
delay(250)
(drawable as? AnimatedVectorDrawable)?.reset()
delay(500)
(drawable as? AnimatedVectorDrawable)?.start()
}
}
})
(imageView.drawable as? AnimatedVectorDrawable)?.start()
}
}
return binding.root
}
override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) {
if (transport != Transport.USB) return
binding.status = status
if (SDK_INT >= 21) {
binding.deviceName =
extras?.getParcelable<UsbDevice>(UsbManager.EXTRA_DEVICE)?.productName ?: "your security key"
} else {
binding.deviceName = "your security key"
}
}
override fun onStart() {
super.onStart()
job = startTransportHandling(Transport.USB)
}
override fun onStop() {
job?.cancel()
super.onStop()
}
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.fido.core.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import org.microg.gms.fido.core.R
import org.microg.gms.fido.core.transport.Transport
import org.microg.gms.fido.core.databinding.FidoWelcomeFragmentBinding
class WelcomeFragment : AuthenticatorActivityFragment() {
private lateinit var binding: FidoWelcomeFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FidoWelcomeFragmentBinding.inflate(inflater, container, false)
binding.data = data
binding.onGetStartedClick = View.OnClickListener {
for (transport in data.supportedTransports) {
if (shouldStartTransportInstantly(transport)) {
startTransportHandling(transport)
return@OnClickListener
}
}
val next = data.supportedTransports.singleOrNull()?.let {
when (it) {
Transport.BLUETOOTH -> R.id.openBluetoothFragmentDirect
Transport.NFC -> R.id.openNfcFragmentDirect
Transport.USB -> R.id.openUsbFragmentDirect
Transport.SCREEN_LOCK -> {
startTransportHandling(Transport.SCREEN_LOCK)
return@OnClickListener
}
}
} ?: R.id.openTransportSelectionFragment
findNavController().navigate(next, arguments.withIsFirst(false))
}
return binding.root
}
}

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="90dp"
android:height="160dp"
android:viewportWidth="90"
android:viewportHeight="160">
<path
android:name="path"
android:fillColor="#757575"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:strokeWidth="1" />
<path
android:name="path_1"
android:fillColor="#545454"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:strokeWidth="2"
android:strokeColor="#cccccc" />
<group android:name="group">
<path
android:name="path_2"
android:fillColor="#d8d8d8"
android:pathData="M 57.072 32.8 L 65.97 40.851 L 57.919 49.749 L 49.021 41.698 Z"
android:strokeWidth="2"
android:strokeColor="#545454" />
<path
android:name="path_3"
android:fillColor="#545454"
android:pathData="M 48.879 38.876 L 60.744 49.61 C 61.004 49.846 61.198 50.145 61.306 50.48 C 61.413 50.814 61.431 51.17 61.357 51.514 C 61.284 51.857 61.121 52.175 60.885 52.435 L 38.745 76.906 C 37.856 77.889 36.611 78.479 35.287 78.545 C 33.963 78.611 32.666 78.148 31.683 77.259 L 24.268 70.55 C 23.285 69.661 22.695 68.416 22.628 67.092 C 22.562 65.768 23.025 64.471 23.914 63.488 L 46.054 39.017 C 46.29 38.757 46.59 38.563 46.924 38.455 C 47.258 38.347 47.615 38.33 47.958 38.403 C 48.301 38.477 48.619 38.64 48.879 38.876 Z"
android:strokeWidth="1" />
<path
android:name="path_4"
android:fillColor="#ddcc44"
android:pathData="M 41.391 59.074 C 40.608 58.366 39.621 57.923 38.571 57.81 C 37.521 57.697 36.462 57.919 35.546 58.445 C 34.63 58.971 33.904 59.773 33.472 60.737 C 33.041 61.7 32.925 62.777 33.142 63.81 C 33.36 64.843 33.899 65.782 34.682 66.49 C 35.465 67.199 36.452 67.641 37.502 67.755 C 38.552 67.868 39.611 67.645 40.527 67.12 C 41.443 66.594 42.169 65.791 42.601 64.828 C 43.033 63.864 43.148 62.788 42.931 61.754 C 42.714 60.721 42.174 59.783 41.391 59.074 Z"
android:strokeWidth="1" />
</group>
</vector>
</aapt:attr>
<target android:name="group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="1000"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="translateY"
android:valueFrom="50"
android:valueTo="0"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="strokeAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="path_4">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,111 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: CC-BY-4.0
-->
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="90dp"
android:height="220dp"
android:viewportWidth="90"
android:viewportHeight="220">
<group
android:name="group"
android:translateY="-22">
<path
android:name="path"
android:fillColor="#d8d8d8"
android:pathData="M 39 170 L 51 170 L 51 182 L 39 182 Z"
android:strokeWidth="2"
android:strokeColor="#545454" />
<path
android:name="path_1"
android:fillColor="#545454"
android:pathData="M 37 180 L 53 180 C 53.53 180 54.039 180.211 54.414 180.586 C 54.789 180.961 55 181.47 55 182 L 55 215 C 55 216.326 54.473 217.598 53.536 218.536 C 52.598 219.473 51.326 220 50 220 L 40 220 C 38.674 220 37.402 219.473 36.464 218.536 C 35.527 217.598 35 216.326 35 215 L 35 182 C 35 181.47 35.211 180.961 35.586 180.586 C 35.961 180.211 36.47 180 37 180 Z"
android:strokeWidth="1" />
<path
android:name="path_2"
android:fillColor="#ddcc44"
android:pathData="M 45 200 C 43.674 200 42.402 200.527 41.464 201.464 C 40.527 202.402 40 203.674 40 205 C 40 206.326 40.527 207.598 41.464 208.536 C 42.402 209.473 43.674 210 45 210 C 46.326 210 47.598 209.473 48.536 208.536 C 49.473 207.598 50 206.326 50 205 C 50 203.674 49.473 202.402 48.536 201.464 C 47.598 200.527 46.326 200 45 200 Z"
android:strokeWidth="1" />
<group android:name="group_1">
<path
android:name="path_3"
android:pathData="M 45 195 C 42.349 195 39.804 196.054 37.929 197.929 C 36.054 199.804 35 202.349 35 205 C 35 207.651 36.054 210.196 37.929 212.071 C 39.804 213.946 42.349 215 45 215 C 47.651 215 50.196 213.946 52.071 212.071 C 53.946 210.196 55 207.651 55 205 C 55 202.349 53.946 199.804 52.071 197.929 C 50.196 196.054 47.651 195 45 195 Z"
android:strokeWidth="1"
android:strokeColor="#ddcc44" />
</group>
</group>
<path
android:name="path_4"
android:fillColor="#757575"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:strokeWidth="1" />
<path
android:name="path_5"
android:fillColor="#545454"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:strokeWidth="2"
android:strokeColor="#cccccc" />
</vector>
</aapt:attr>
<target android:name="group_1">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="translateX"
android:valueFrom="22.5"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="translateY"
android:valueFrom="102.5"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_3">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="500"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="strokeAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="500"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="strokeAlpha"
android:startOffset="500"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,68 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: CC-BY-4.0
-->
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="90dp"
android:height="220dp"
android:viewportWidth="90"
android:viewportHeight="220">
<group android:name="group">
<path
android:name="path"
android:pathData="M 39 170 L 51 170 L 51 182 L 39 182 Z"
android:fillColor="#d8d8d8"
android:strokeColor="#545454"
android:strokeWidth="2"/>
<path
android:name="path_1"
android:pathData="M 37 180 L 53 180 C 53.53 180 54.039 180.211 54.414 180.586 C 54.789 180.961 55 181.47 55 182 L 55 215 C 55 216.326 54.473 217.598 53.536 218.536 C 52.598 219.473 51.326 220 50 220 L 40 220 C 38.674 220 37.402 219.473 36.464 218.536 C 35.527 217.598 35 216.326 35 215 L 35 182 C 35 181.47 35.211 180.961 35.586 180.586 C 35.961 180.211 36.47 180 37 180 Z"
android:fillColor="#545454"
android:strokeWidth="1"/>
<path
android:name="path_2"
android:pathData="M 45 200 C 43.674 200 42.402 200.527 41.464 201.464 C 40.527 202.402 40 203.674 40 205 C 40 206.326 40.527 207.598 41.464 208.536 C 42.402 209.473 43.674 210 45 210 C 46.326 210 47.598 209.473 48.536 208.536 C 49.473 207.598 50 206.326 50 205 C 50 203.674 49.473 202.402 48.536 201.464 C 47.598 200.527 46.326 200 45 200 Z"
android:fillColor="#ddcc44"
android:strokeWidth="1"/>
</group>
<path
android:name="path_4"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:fillColor="#757575"
android:strokeWidth="1"/>
<path
android:name="path_5"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:fillColor="#545454"
android:strokeColor="#cccccc"
android:strokeWidth="2"/>
</vector>
</aapt:attr>
<target android:name="group">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="translateY"
android:duration="600"
android:valueFrom="0"
android:valueTo="-16"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator
android:propertyName="translateY"
android:startOffset="600"
android:duration="200"
android:valueFrom="-16"
android:valueTo="-22"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set>
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="90dp"
android:height="160dp"
android:viewportWidth="90"
android:viewportHeight="160">
<path
android:name="path"
android:fillColor="#757575"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:strokeWidth="1" />
<path
android:name="path_1"
android:fillColor="#545454"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:strokeWidth="2"
android:strokeColor="#cccccc" />
<group android:name="group">
<path
android:name="path_2"
android:fillColor="#d8d8d8"
android:pathData="M 57.072 32.8 L 65.97 40.851 L 57.919 49.749 L 49.021 41.698 Z"
android:strokeWidth="2"
android:strokeColor="#545454" />
<path
android:name="path_3"
android:fillColor="#545454"
android:pathData="M 48.879 38.876 L 60.744 49.61 C 61.004 49.846 61.198 50.145 61.306 50.48 C 61.413 50.814 61.431 51.17 61.357 51.514 C 61.284 51.857 61.121 52.175 60.885 52.435 L 38.745 76.906 C 37.856 77.889 36.611 78.479 35.287 78.545 C 33.963 78.611 32.666 78.148 31.683 77.259 L 24.268 70.55 C 23.285 69.661 22.695 68.416 22.628 67.092 C 22.562 65.768 23.025 64.471 23.914 63.488 L 46.054 39.017 C 46.29 38.757 46.59 38.563 46.924 38.455 C 47.258 38.347 47.615 38.33 47.958 38.403 C 48.301 38.477 48.619 38.64 48.879 38.876 Z"
android:strokeWidth="1" />
<path
android:name="path_4"
android:fillColor="#ddcc44"
android:pathData="M 41.391 59.074 C 40.608 58.366 39.621 57.923 38.571 57.81 C 37.521 57.697 36.462 57.919 35.546 58.445 C 34.63 58.971 33.904 59.773 33.472 60.737 C 33.041 61.7 32.925 62.777 33.142 63.81 C 33.36 64.843 33.899 65.782 34.682 66.49 C 35.465 67.199 36.452 67.641 37.502 67.755 C 38.552 67.868 39.611 67.645 40.527 67.12 C 41.443 66.594 42.169 65.791 42.601 64.828 C 43.033 63.864 43.148 62.788 42.931 61.754 C 42.714 60.721 42.174 59.783 41.391 59.074 Z"
android:strokeWidth="1" />
</group>
</vector>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: CC-BY-4.0
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="90dp"
android:height="220dp"
android:viewportWidth="90"
android:viewportHeight="220">
<group
android:name="group"
android:translateY="-22">
<path
android:name="path"
android:fillColor="#d8d8d8"
android:pathData="M 39 170 L 51 170 L 51 182 L 39 182 Z"
android:strokeWidth="2"
android:strokeColor="#545454" />
<path
android:name="path_1"
android:fillColor="#545454"
android:pathData="M 37 180 L 53 180 C 53.53 180 54.039 180.211 54.414 180.586 C 54.789 180.961 55 181.47 55 182 L 55 215 C 55 216.326 54.473 217.598 53.536 218.536 C 52.598 219.473 51.326 220 50 220 L 40 220 C 38.674 220 37.402 219.473 36.464 218.536 C 35.527 217.598 35 216.326 35 215 L 35 182 C 35 181.47 35.211 180.961 35.586 180.586 C 35.961 180.211 36.47 180 37 180 Z"
android:strokeWidth="1" />
<path
android:name="path_2"
android:fillColor="#ddcc44"
android:pathData="M 45 200 C 43.674 200 42.402 200.527 41.464 201.464 C 40.527 202.402 40 203.674 40 205 C 40 206.326 40.527 207.598 41.464 208.536 C 42.402 209.473 43.674 210 45 210 C 46.326 210 47.598 209.473 48.536 208.536 C 49.473 207.598 50 206.326 50 205 C 50 203.674 49.473 202.402 48.536 201.464 C 47.598 200.527 46.326 200 45 200 Z"
android:strokeWidth="1" />
<group
android:name="group_1"
android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="11.25"
android:translateY="51.25">
<path
android:name="path_3"
android:pathData="M 45 195 C 42.349 195 39.804 196.054 37.929 197.929 C 36.054 199.804 35 202.349 35 205 C 35 207.651 36.054 210.196 37.929 212.071 C 39.804 213.946 42.349 215 45 215 C 47.651 215 50.196 213.946 52.071 212.071 C 53.946 210.196 55 207.651 55 205 C 55 202.349 53.946 199.804 52.071 197.929 C 50.196 196.054 47.651 195 45 195 Z"
android:strokeWidth="1"
android:strokeColor="#ddcc44" />
</group>
</group>
<path
android:name="path_4"
android:fillColor="#757575"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:strokeWidth="1" />
<path
android:name="path_5"
android:fillColor="#545454"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:strokeWidth="2"
android:strokeColor="#cccccc" />
</vector>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: CC-BY-4.0
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="90dp"
android:height="220dp"
android:viewportWidth="90"
android:viewportHeight="220">
<group
android:name="group"
android:translateY="-16">
<path
android:name="path"
android:fillColor="#d8d8d8"
android:pathData="M 39 170 L 51 170 L 51 182 L 39 182 Z"
android:strokeWidth="2"
android:strokeColor="#545454" />
<path
android:name="path_1"
android:fillColor="#545454"
android:pathData="M 37 180 L 53 180 C 53.53 180 54.039 180.211 54.414 180.586 C 54.789 180.961 55 181.47 55 182 L 55 215 C 55 216.326 54.473 217.598 53.536 218.536 C 52.598 219.473 51.326 220 50 220 L 40 220 C 38.674 220 37.402 219.473 36.464 218.536 C 35.527 217.598 35 216.326 35 215 L 35 182 C 35 181.47 35.211 180.961 35.586 180.586 C 35.961 180.211 36.47 180 37 180 Z"
android:strokeWidth="1" />
<path
android:name="path_2"
android:fillColor="#ddcc44"
android:pathData="M 45 200 C 43.674 200 42.402 200.527 41.464 201.464 C 40.527 202.402 40 203.674 40 205 C 40 206.326 40.527 207.598 41.464 208.536 C 42.402 209.473 43.674 210 45 210 C 46.326 210 47.598 209.473 48.536 208.536 C 49.473 207.598 50 206.326 50 205 C 50 203.674 49.473 202.402 48.536 201.464 C 47.598 200.527 46.326 200 45 200 Z"
android:strokeWidth="1" />
</group>
<path
android:name="path_4"
android:fillColor="#757575"
android:pathData="M 10 0 L 80 0 C 82.651 0 85.196 1.054 87.071 2.929 C 88.946 4.804 90 7.349 90 10 L 90 150 C 90 152.651 88.946 155.196 87.071 157.071 C 85.196 158.946 82.651 160 80 160 L 10 160 C 7.349 160 4.804 158.946 2.929 157.071 C 1.054 155.196 0 152.651 0 150 L 0 10 C 0 7.349 1.054 4.804 2.929 2.929 C 4.804 1.054 7.349 0 10 0"
android:strokeWidth="1" />
<path
android:name="path_5"
android:fillColor="#545454"
android:pathData="M 15 10 C 13.674 10 12.402 10.527 11.464 11.464 C 10.527 12.402 10 13.674 10 15 C 10 16.326 10.527 17.598 11.464 18.536 C 12.402 19.473 13.674 20 15 20 C 16.326 20 17.598 19.473 18.536 18.536 C 19.473 17.598 20 16.326 20 15 C 20 13.674 19.473 12.402 18.536 11.464 C 17.598 10.527 16.326 10 15 10 Z"
android:strokeWidth="2"
android:strokeColor="#cccccc" />
</vector>

View file

@ -0,0 +1,16 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88z" />
</vector>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorOnPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z" />
</vector>

View file

@ -0,0 +1,16 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z" />
</vector>

View file

@ -0,0 +1,16 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View file

@ -0,0 +1,16 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" />
</vector>

View file

@ -0,0 +1,16 @@
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M15,7v4h1v2h-3V5h2l-3,-4 -3,4h2v8H8v-2.07c0.7,-0.37 1.2,-1.08 1.2,-1.93 0,-1.21 -0.99,-2.2 -2.2,-2.2 -1.21,0 -2.2,0.99 -2.2,2.2 0,0.85 0.5,1.56 1.2,1.93V13c0,1.11 0.89,2 2,2h3v3.05c-0.71,0.37 -1.2,1.1 -1.2,1.95 0,1.22 0.99,2.2 2.2,2.2 1.21,0 2.2,-0.98 2.2,-2.2 0,-0.85 -0.49,-1.58 -1.2,-1.95V15h3c1.11,0 2,-0.89 2,-2v-2h1V7h-4z" />
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="org.microg.gms.fido.core.transport.Transport" />
<import type="org.microg.gms.fido.core.transport.TransportHandlerCallback" />
<variable
name="data"
type="org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData" />
<variable
name="status"
type="String" />
<variable
name="onBackClick"
type="android.view.View.OnClickListener" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_key" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:text="@{data.isFirst ? @string/fido_welcome_title(data.appName) : @string/fido_nfc_title}"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="@string/fido_nfc_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/fido_nfc_prompt_body"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@{@string/fido_welcome_body(data.appName)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="@{data.isFirst ? View.VISIBLE : View.GONE}"
tools:text="@string/fido_welcome_body" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{status == TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE ? View.VISIBLE : View.GONE}">
<ImageView
android:id="@+id/fido_nfc_wait_connect_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="240dp"
android:scaleType="fitCenter"
android:src="@drawable/fido_nfc_wait_connect" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom|start"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.size() > 1 ? View.VISIBLE : View.GONE}">
<Button
android:id="@android:id/button1"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:onClick="@{onBackClick}"
android:text="@string/fido_transport_modify"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="onEnterPin"
type="android.view.View.OnClickListener" />
<variable
name="onCancel"
type="android.view.View.OnClickListener" />
<variable
name="onInputChange"
type="androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_key" />
<TextView
android:id="@+id/pin_fragment_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:text="@string/fido_pin_title"
android:textAppearance="@style/TextAppearance.AppCompat.Title" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/fido_pin_hint">
<requestFocus />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin_editor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="textPassword"
android:afterTextChanged="@{onInputChange}"
android:imeOptions="actionDone"
android:autofillHints="password" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_marginTop="24dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom|end"
android:orientation="horizontal">
<Button
android:id="@+id/pin_fragment_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fido_pin_cancel"
android:onClick="@{onCancel}"
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
<Button
android:id="@+id/pin_fragment_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fido_pin_ok"
android:onClick="@{onEnterPin}"
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/sign_in_key_logo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_fido_key" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/sign_in_key_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
<TextView
android:id="@+id/sign_in_key_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="2dp"
android:textSize="12sp" />
<TextView
android:id="@+id/sign_in_key_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fido_sign_in_selection_description"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:id="@+id/sign_in_key_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
android:layout_gravity="end"
android:src="@drawable/ic_fido_close_btn" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:gravity="center"
android:textSize="24dp"
android:text="@string/fido_sign_in_selection_title"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="@string/fido_sign_in_selection_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="18dp"
android:text="@{@string/fido_sign_in_selection_continue(data.appName)}"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="@string/fido_sign_in_selection_continue" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sign_in_key_recycler"
android:layout_marginVertical="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>

View file

@ -0,0 +1,226 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="org.microg.gms.fido.core.transport.Transport" />
<variable
name="data"
type="org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData" />
<variable
name="onBluetoothClick"
type="android.view.View.OnClickListener" />
<variable
name="onNfcClick"
type="android.view.View.OnClickListener" />
<variable
name="onUsbClick"
type="android.view.View.OnClickListener" />
<variable
name="onScreenLockClick"
type="android.view.View.OnClickListener" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_key" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:text="@{data.isFirst ? @string/fido_welcome_title(data.appName): @string/fido_transport_selection_title}"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="@string/fido_transport_selection_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/fido_transport_selection_body"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="@{data.implementedTransports.contains(Transport.BLUETOOTH)}"
android:focusable="@{data.implementedTransports.contains(Transport.BLUETOOTH)}"
android:alpha="@{data.implementedTransports.contains(Transport.BLUETOOTH) ? 1.0f : 0.5f}"
android:onClick="@{onBluetoothClick}"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.contains(Transport.BLUETOOTH) ? View.VISIBLE : View.GONE}">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_bluetooth" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/fido_transport_selection_bluetooth"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="@{data.implementedTransports.contains(Transport.NFC)}"
android:focusable="@{data.implementedTransports.contains(Transport.NFC)}"
android:alpha="@{data.implementedTransports.contains(Transport.NFC) ? 1.0f : 0.5f}"
android:onClick="@{onNfcClick}"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.contains(Transport.NFC) ? View.VISIBLE : View.GONE}">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_nfc" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/fido_transport_selection_nfc"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="@{data.implementedTransports.contains(Transport.USB)}"
android:focusable="@{data.implementedTransports.contains(Transport.USB)}"
android:alpha="@{data.implementedTransports.contains(Transport.USB) ? 1.0f : 0.5f}"
android:onClick="@{onUsbClick}"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.contains(Transport.USB) ? View.VISIBLE : View.GONE}">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_usb" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/fido_transport_selection_usb"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="@{data.implementedTransports.contains(Transport.SCREEN_LOCK)}"
android:focusable="@{data.implementedTransports.contains(Transport.SCREEN_LOCK)}"
android:alpha="@{data.implementedTransports.contains(Transport.SCREEN_LOCK) ? 1.0f : 0.5f}"
android:onClick="@{onScreenLockClick}"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.contains(Transport.SCREEN_LOCK) ? View.VISIBLE : View.GONE}">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_fingerprint" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/fido_transport_selection_biometric"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="org.microg.gms.fido.core.transport.Transport" />
<import type="org.microg.gms.fido.core.transport.TransportHandlerCallback" />
<variable
name="data"
type="org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData" />
<variable
name="status"
type="String" />
<variable
name="deviceName"
type="String" />
<variable
name="onBackClick"
type="android.view.View.OnClickListener" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_key" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:text="@{data.isFirst ? @string/fido_welcome_title(data.appName) : @string/fido_usb_title}"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="@string/fido_usb_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/fido_usb_prompt_body"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@{@string/fido_welcome_body(data.appName)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="@{data.isFirst ? View.VISIBLE : View.GONE}"
tools:text="@string/fido_welcome_body" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{status == TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE ? View.VISIBLE : View.GONE}">
<ImageView
android:id="@+id/fido_usb_wait_connect_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="240dp"
android:scaleType="fitCenter"
android:src="@drawable/fido_usb_wait_connect" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/fido_transport_usb_wait_connect_body" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{status == TransportHandlerCallback.STATUS_WAITING_FOR_USER ? View.VISIBLE : View.GONE}"
tools:visibility="gone">
<ImageView
android:id="@+id/fido_usb_wait_confirm_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="240dp"
android:scaleType="fitCenter"
android:src="@drawable/fido_usb_wait_confirm" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text='@{@string/fido_transport_usb_wait_confirm_body(deviceName)}'
tools:text="@string/fido_transport_usb_wait_confirm_body" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom|start"
android:orientation="horizontal"
android:visibility="@{data.supportedTransports.size() > 1 ? View.VISIBLE : View.GONE}">
<Button
android:id="@android:id/button1"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:onClick="@{onBackClick}"
android:text="@string/fido_transport_modify"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="org.microg.gms.fido.core.ui.AuthenticatorActivityFragmentData" />
<variable
name="onGetStartedClick"
type="android.view.View.OnClickListener" />
<import type="android.view.View" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitXY"
android:src="@drawable/ic_fido_key" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="8dp"
android:text="@{@string/fido_welcome_title(data.appName)}"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="@string/fido_welcome_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{@string/fido_welcome_body(data.appName)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
tools:text="@string/fido_welcome_body" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="@{data.privilegedCallerName != null ? View.VISIBLE : View.GONE}"
android:text="@{@string/fido_welcome_privileged_info(data.privilegedCallerName, data.appName)}"
tools:text="@string/fido_welcome_privileged_info" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/privilegedCheck"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:visibility="@{data.requiresPrivilege ? View.VISIBLE : View.GONE}"
android:text="@{@string/fido_welcome_privileged_check(data.privilegedCallerName)}"
tools:text="@string/fido_welcome_privileged_check" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:gravity="bottom|end"
android:orientation="horizontal">
<Button
android:id="@android:id/button1"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="@{privilegedCheck.checked || !data.requiresPrivilege}"
android:onClick="@{onGetStartedClick}"
android:text="@string/fido_welcome_button_get_started" />
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_fido_authenticator"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="org.microg.gms.fido.core.ui.WelcomeFragment"
tools:layout="@layout/fido_welcome_fragment">
<argument
android:name="appName"
app:argType="string" />
<action
android:id="@+id/openTransportSelectionFragment"
app:destination="@id/transportSelectionFragment" />
<action
android:id="@+id/openBluetoothFragmentDirect"
app:destination="@id/bluetoothFragment" />
<action
android:id="@+id/openNfcFragmentDirect"
app:destination="@id/nfcFragment" />
<action
android:id="@+id/openUsbFragmentDirect"
app:destination="@id/usbFragment" />
</fragment>
<fragment
android:id="@+id/transportSelectionFragment"
android:name="org.microg.gms.fido.core.ui.TransportSelectionFragment"
tools:layout="@layout/fido_transport_selection_fragment">
<argument
android:name="appName"
app:argType="string" />
<argument
android:name="isFirst"
app:argType="boolean" />
<action
android:id="@+id/openBluetoothFragment"
app:destination="@id/bluetoothFragment" />
<action
android:id="@+id/openNfcFragment"
app:destination="@id/nfcFragment" />
<action
android:id="@+id/openUsbFragment"
app:destination="@id/usbFragment" />
</fragment>
<fragment android:id="@+id/bluetoothFragment" />
<fragment
android:id="@+id/nfcFragment"
android:name="org.microg.gms.fido.core.ui.NfcTransportFragment"
tools:layout="@layout/fido_nfc_transport_fragment">
<action
android:id="@+id/openPinFragment"
app:destination="@id/pinFragment" />
</fragment>
<fragment
android:id="@+id/usbFragment"
android:name="org.microg.gms.fido.core.ui.UsbTransportFragment"
tools:layout="@layout/fido_usb_transport_fragment">
<action
android:id="@+id/openPinFragment"
app:destination="@id/pinFragment" />
</fragment>
<fragment
android:id="@+id/pinFragment"
android:name="org.microg.gms.fido.core.ui.PinFragment"
tools:layout="@layout/fido_pin_fragment" />
<fragment
android:id="@+id/signInSelectionFragment"
android:name="org.microg.gms.fido.core.ui.SignInSelectionFragment"
tools:layout="@layout/fido_sign_in_selection_fragment">
<action
android:id="@+id/openWelcomeFragment"
app:destination="@id/welcomeFragment" />
</fragment>
</navigation>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">استخدم مفتاح أمانك مع %1$s</string>
<string name="fido_welcome_body">استخدام مفتاح أمانك مع %1$s يساهم في حماية بياناتك الشخصية.</string>
<string name="fido_welcome_button_get_started">ابدأ</string>
<string name="fido_welcome_privileged_info">%1$s يعمل كمتصفح موثوق لاستخدام مفتاح أمانك مع %2$s.</string>
<string name="fido_welcome_privileged_check">نعم، %1$s متصفحي الموثوق ويجب السماح له باستخدام مفاتيح الأمان مع مواقع خارجية.</string>
<string name="fido_transport_selection_title">اختر كيف تستخدم مفتاح أمانك</string>
<string name="fido_transport_selection_body">مفاتيح الأمان تعمل مع البلوتوث وNFC وUSB. اختر كيف تريد استخدام مفتاحك.</string>
<string name="fido_usb_prompt_body">قم بتوصيل مفتاح أمانك إلى منفذ الـ USB أو وصله بسلك USB. إذا كان لمفتاحك زر أو قرص ذهبي، اضغط عليه الآن.</string>
<string name="fido_nfc_title">قم بتوصيل مفتاح أمان الـ NFC الخاص بك</string>
<string name="fido_transport_selection_bluetooth">استخدم مفتاح الأمان مع البلوتوث</string>
<string name="fido_transport_selection_nfc">استخدم مفتاح الأمان مع الـ NFC</string>
<string name="fido_transport_selection_biometric">استخدم هذا الجهاز بقفل الشاشة</string>
<string name="fido_transport_usb_wait_connect_body">يرجى توصيل مفتاح أمان الـ USB الخاص بك.</string>
<string name="fido_biometric_prompt_title">تحقق من هويتك</string>
<string name="fido_biometric_prompt_body">يحتاج %1$s إلى أن يتحقق من هويتك.</string>
<string name="fido_usb_title">قم بتوصيل مفتاح أمان الـ USB الخاص بك</string>
<string name="fido_transport_selection_usb">استخدم مفتاح الأمان مع الـ USB</string>
<string name="fido_nfc_prompt_body">امسك مفتاحك مستويًا على ظهر جهازك حتى يتوقف الاهتزاز</string>
<string name="fido_transport_usb_wait_confirm_body">يرجى الضغط على الحلقة الذهبية أو القرص على %1$s.</string>
<string name="fido_pin_hint">من 4 إلى 63 حرفًا</string>
<string name="fido_pin_ok">موافق</string>
<string name="fido_pin_cancel">إلغاء</string>
<string name="fido_wrong_pin">رقم سري خاطئ!</string>
<string name="fido_pin_title">يرجى إدخال الرقم السري لجهاز المصادقتك</string>
<string name="fido_transport_modify">غير طريقة استخدامك لمفتاح اﻷمان</string>
</resources>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">Comenzar</string>
<string name="fido_usb_prompt_body">Conecta la llave de seguranza al puertu USB o conéctala con un cable USB. Si la llave tien un botón o una aniella dorada, tócalos yá.</string>
<string name="fido_transport_usb_wait_connect_body">Conecta la to llave USB de seguranza.</string>
<string name="fido_nfc_prompt_body">Ten arimada la llave a la parte trasera del preséu hasta qu\'esti dexe de vibrar</string>
<string name="fido_welcome_body">Usar la to llave de seguranza con «%1$s» ayuda a protexer los tos datos privaos.</string>
<string name="fido_welcome_title">Usu de llaves de seguranza con «%1$s»</string>
<string name="fido_transport_selection_biometric">Usar esti preséu col bloquéu de pantalla</string>
<string name="fido_welcome_privileged_check">Sí, «%1$s» ye\'l mio restolador d\'enfotu ya habría tener permisu pa usar llaves de seguranza con sitios web de terceros.</string>
<string name="fido_biometric_prompt_body">«%1$s» tien de verificar que yes tu.</string>
<string name="fido_transport_selection_usb">Usar la llave col USB</string>
<string name="fido_transport_selection_title">Cómo usar la llave de seguranza</string>
<string name="fido_biometric_prompt_title">Verificación de la identidá</string>
<string name="fido_usb_title">Conexón per USB</string>
<string name="fido_transport_usb_wait_confirm_body">Toca l\'aniella dorada o\'l discu de: %1$s.</string>
<string name="fido_welcome_privileged_info">«%1$s» actúa como un restolador d\'enfotu pa usar la to llave de seguranza con «%2$s».</string>
<string name="fido_nfc_title">Conexón per NFC</string>
<string name="fido_transport_selection_bluetooth">Usar la llave col bluetooth</string>
<string name="fido_transport_selection_body">Les llaves de seguranza funcionen con bluetooth, NFC y USB. Escueyi cómo quies usar la to llave.</string>
<string name="fido_transport_selection_nfc">Usar la llave col NFC</string>
</resources>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_pin_title">Xahiş edirik, təsdiqləyiciniz üçün PIN daxil edin</string>
<string name="fido_pin_hint">4 - 63 simvol</string>
<string name="fido_pin_ok">Oldu</string>
<string name="fido_pin_cancel">Ləğv et</string>
<string name="fido_nfc_prompt_body">Açarınızı titrəməyi dayandırana kimi cihazınızın arxasına düz tutun</string>
<string name="fido_transport_selection_bluetooth">Bluetooth ilə təhlükəsizlik açarın istifadə edin</string>
<string name="fido_transport_selection_nfc">NFC ilə təhlükəsizlik açarın istifadə edin</string>
<string name="fido_transport_selection_usb">Təhlükəsizlik açarın USB ilə istifadə et</string>
<string name="fido_transport_selection_biometric">Bu cihazı ekran kilidi ilə istifadə et</string>
<string name="fido_transport_usb_wait_connect_body">USB təhlükəsizlik açarınızı qoşun.</string>
<string name="fido_transport_usb_wait_confirm_body">%1$s üzərində qızıl üzük və ya diskə toxunun.</string>
<string name="fido_welcome_title">Təhlükəsizlik açarınızı %1$s ilə istifadə edin</string>
<string name="fido_welcome_body">Təhlükəsizlik açarınızın %1$s ilə istifadəsi şəxsi məlumatınızı qorumağa kömək edir.</string>
<string name="fido_welcome_button_get_started">Başla</string>
<string name="fido_welcome_privileged_info">%1$s təhlükəsizlik açarınızı %2$s ilə istifadə etmək üçün etibarlı brauzer kimi işləyir.</string>
<string name="fido_welcome_privileged_check">Bəli, %1$s mənim etibarlı brauzerimdir və üçüncü tərəf veb saytları ilə təhlükəsizlik açarların işlətməyə icazə verməlidir.</string>
<string name="fido_transport_selection_title">Təhlükəsizlik açarınızı necə istifadə edəcəyinizi seçin</string>
<string name="fido_transport_selection_body">Təhlükəsizlik açarları Bluetooth, NFC və USB ilə işləyir. Açarınızı necə istifadə edəcəyinizi seçin.</string>
<string name="fido_biometric_prompt_title">Kim olduğunuzu təsdiqləyin</string>
<string name="fido_biometric_prompt_body">%1$s kimliyinizi təsdiqləməlidir.</string>
<string name="fido_usb_title">USB təhlükəsizlik açarınızı qoşun</string>
<string name="fido_usb_prompt_body">Təhlükəsizlik açarınızı USB portuna qoşun və ya USB kabel ilə əlaqələndirin. Açarınızın düyməsi və ya qızılı diski varsa, indi ona toxunun.</string>
<string name="fido_nfc_title">NFC təhlükəsizlik açarınızı qoşun</string>
<string name="fido_wrong_pin">Səhv PIN daxil edilib!</string>
</resources>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources>
<string name="fido_welcome_title">Выкарыстоўваць ваш ключ бяспекі для %1$s</string>
<string name="fido_welcome_body">Выкарыстанне ключа бяспекі для %1$s дапамагае абараніць вашыя асабістыя дадзеныя.</string>
<string name="fido_welcome_button_get_started">Пачынаем працу</string>
<string name="fido_welcome_privileged_info">%1$s выступае ў якасці даверанага браўзэра для выкарыстання вашага ключа бяспекі з %2$s.</string>
<string name="fido_welcome_privileged_check">Так, %1$s мой давераны браўзэр, і яму павінна быць дазволена выкарыстоўваць ключы бяспекі на іншых вэб-сайтах.</string>
<string name="fido_transport_selection_title">Выберыце, як выкарыстоўваць ключ бяспекі</string>
<string name="fido_transport_selection_body">Ключы бяспекі працуюць з Bluetooth, NFC і USB. Выберыце, як вы хочаце выкарыстоўваць свой ключ.</string>
<string name="fido_biometric_prompt_title">Пацвердзіце сваю асобу</string>
<string name="fido_biometric_prompt_body">"%1$s павінен пацвердзіць, што гэта вы."</string>
<string name="fido_usb_title">Падключыце USB ключ бяспекі</string>
<string name="fido_usb_prompt_body">Падключыце ключ бяспекі да USB-парта або з дапамогай USB-кабеля. Калі на вашым ключы ёсць кнопка, дакраніцеся яе.</string>
<string name="fido_nfc_title">Падключыце свой NFC ключ бяспекі</string>
<string name="fido_nfc_prompt_body">Прыцісніце ключ да задняй панэлі прылады, пакуль яно не перастане вібраваць</string>
<string name="fido_transport_selection_bluetooth">Выкарыстоўваць ключ бяспекі з Bluetooth</string>
<string name="fido_transport_selection_nfc">Выкарыстоўваць ключ бяспекі з NFC</string>
<string name="fido_transport_selection_usb">Выкарыстоўваць ключ бяспекі з USB</string>
<string name="fido_transport_selection_biometric">Выкарыстоўваць гэтую прыладу з заблакаваным экранам</string>
<string name="fido_transport_usb_wait_connect_body">Калі ласка, падключыце ваш USB-ключ бяспекі.</string>
<string name="fido_transport_usb_wait_confirm_body">Калі ласка, краніце залатога кальца або дыска на %1$s.</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_wrong_pin">Няправільны PIN-код!</string>
<string name="fido_pin_title">Увядзіце PIN-код вашага аўтэнтыфікатара</string>
<string name="fido_pin_cancel">Адмена</string>
<string name="fido_pin_hint">Ад 4 да 63 сімвалаў</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Použijte svůj bezpečnostní klíč s %1$s</string>
<string name="fido_welcome_body">Použitím svého bezpečnostního klíče s %1$s lépe ochráníte svá soukromá data.</string>
<string name="fido_welcome_privileged_info">%1$s funguje jako důvěryhodný prohlížeč, který používá váš bezpečnostní klíč s %2$s.</string>
<string name="fido_transport_selection_title">Zvolte, jak chcete používat váš bezpečnostní klíč</string>
<string name="fido_transport_selection_body">Bezpečnostní klíče fungují přes Bluetooth, NFC a USB. Zvolte, jak chcete používat váš klíč.</string>
<string name="fido_biometric_prompt_body">%1$s potřebuje ověřit, že jste to vy.</string>
<string name="fido_usb_prompt_body">Připojte svůj bezpečnostní klíč do USB portu nebo jej připojte pomocí USB kabelu. Pokud je váš klíč vybaven tlačítkem nebo zlatým kruhem, klepněte na něj.</string>
<string name="fido_nfc_title">Připojte svůj NFC bezpečnostní klíč</string>
<string name="fido_transport_selection_bluetooth">Použít bezpečnostní klíč s Bluetooth</string>
<string name="fido_transport_selection_nfc">Použít bezpečnostní klíč s NFC</string>
<string name="fido_transport_selection_usb">Použít bezpečnostní klíč s USB</string>
<string name="fido_transport_selection_biometric">Použít toto zařízení se zámkem obrazovky</string>
<string name="fido_transport_usb_wait_connect_body">Připojte prosím svůj USB bezpečnostní klíč.</string>
<string name="fido_transport_usb_wait_confirm_body">Klepněte prosím na zlatý kruh na %1$s.</string>
<string name="fido_welcome_button_get_started">Začínáme</string>
<string name="fido_welcome_privileged_check">Ano, %1$s je můj důvěryhodný prohlížeč a měl by mít přístup k používání bezpečnostních klíčů s webovými stránkami třetích stran.</string>
<string name="fido_biometric_prompt_title">Ověřte svou totožnost</string>
<string name="fido_usb_title">Připojte svůj bezpečnostní USB klíč</string>
<string name="fido_nfc_prompt_body">Podržte klávesu naplocho na zadní straně zařízení, dokud zařízení nepřestane vibrovat</string>
<string name="fido_pin_title">Zadejte prosím PIN vašeho autentifikátoru</string>
<string name="fido_pin_hint">4 až 63 znaků</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_wrong_pin">Zadán nesprávný PIN!</string>
<string name="fido_pin_cancel">Zrušit</string>
<string name="fido_transport_modify">Změnit používání bezpečnostního klíče</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_usb_title">Schließe deinen USB-Sicherheitsschlüssel an</string>
<string name="fido_welcome_title">Verwende deinen Sicherheitsschlüssel mit %1$s</string>
<string name="fido_welcome_privileged_info">%1$s fungiert als vertrauenswürdiger Browser zur Verwendung deines Sicherheitsschlüssels mit %2$s.</string>
<string name="fido_transport_selection_body">Sicherheitsschlüssel funktionieren mit Bluetooth, NFC und USB. Wähle aus, wie du deinen Schlüssel verwenden möchtest.</string>
<string name="fido_usb_prompt_body">Stecke deinen Sicherheitsschlüssel in den USB-Anschluss oder verbinde ihn mit einem USB-Kabel. Wenn dein Schlüssel eine Taste oder eine goldene Scheibe hat, tippe jetzt darauf.</string>
<string name="fido_transport_usb_wait_confirm_body">Bitte tippe auf den goldenen Ring oder die goldene Scheibe auf %1$s.</string>
<string name="fido_welcome_body">Die Verwendung deines Sicherheitsschlüssels mit %1$s hilft, deine privaten Daten zu schützen.</string>
<string name="fido_welcome_button_get_started">Los gehts</string>
<string name="fido_welcome_privileged_check">Ja, %1$s ist mein vertrauenswürdiger Browser und sollte Sicherheitsschlüssel für Webseiten von Drittanbietern verwenden dürfen.</string>
<string name="fido_transport_selection_title">Wähle aus, wie du deinen Sicherheitsschlüssel verwendest</string>
<string name="fido_biometric_prompt_title">Verifiziere deine Identität</string>
<string name="fido_biometric_prompt_body">%1$s muss verifizieren, dass du es bist.</string>
<string name="fido_nfc_title">Verbinde deinen NFC-Sicherheitsschlüssel</string>
<string name="fido_nfc_prompt_body">Halte deinen Schlüssel flach auf die Rückseite deines Geräts, bis es aufhört zu vibrieren</string>
<string name="fido_transport_selection_bluetooth">Sicherheitsschlüssel mit Bluetooth verwenden</string>
<string name="fido_transport_selection_nfc">Sicherheitsschlüssel mit NFC verwenden</string>
<string name="fido_transport_selection_usb">Sicherheitsschlüssel mit USB verwenden</string>
<string name="fido_transport_selection_biometric">Dieses Gerät mit Bildschirmsperre verwenden</string>
<string name="fido_transport_usb_wait_connect_body">Bitte schließe deinen USB-Sicherheitsschlüssel an.</string>
<string name="fido_pin_title">Bitte gib die PIN für deinen Authentifikator ein</string>
<string name="fido_pin_hint">4 bis 63 Zeichen</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Abbrechen</string>
<string name="fido_wrong_pin">Falsche PIN eingegeben!</string>
<string name="fido_transport_modify">Ändere, wie du deinen Sicherheitsschlüssel verwendest</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Utilice su clave de seguridad con %1$s</string>
<string name="fido_transport_selection_body">Las llaves de seguridad funcionan con Bluetooth, NFC y USB. Elija cómo quiere usar su llave.</string>
<string name="fido_welcome_body">Utilizar su llave de seguridad con %1$s le ayuda a proteger sus datos privados.</string>
<string name="fido_welcome_button_get_started">Empezar</string>
<string name="fido_welcome_privileged_info">%1$s actúa como navegador de confianza para utilizar su clave de seguridad con %2$s.</string>
<string name="fido_welcome_privileged_check">Sí, %1$s es mi navegador de confianza y debería poder utilizar claves de seguridad con sitios web de terceros.</string>
<string name="fido_transport_selection_title">Elija cómo utilizar su clave de seguridad</string>
<string name="fido_biometric_prompt_title">Compruebe su identidad</string>
<string name="fido_biometric_prompt_body">%1$s necesita verificar que se trata de usted.</string>
<string name="fido_usb_title">Conecte su llave de seguridad USB</string>
<string name="fido_usb_prompt_body">Conecte su llave de seguridad al puerto USB o conéctela con un cable USB. Si su llave tiene un botón o un disco dorado, tóquelo ahora.</string>
<string name="fido_nfc_title">Conecte su llave de seguridad NFC</string>
<string name="fido_nfc_prompt_body">Mantén la llave apoyada en la parte posterior del dispositivo hasta que deje de vibrar</string>
<string name="fido_transport_selection_bluetooth">Utilizar la clave de seguridad con Bluetooth</string>
<string name="fido_transport_selection_nfc">Utilizar la clave de seguridad con NFC</string>
<string name="fido_transport_selection_usb">Utilizar la llave de seguridad con USB</string>
<string name="fido_transport_selection_biometric">Utilizar este dispositivo con bloqueo de pantalla</string>
<string name="fido_transport_usb_wait_connect_body">Conecte su llave de seguridad USB.</string>
<string name="fido_transport_usb_wait_confirm_body">Toque el anillo o disco dorado en %1$s.</string>
<string name="fido_pin_hint">De 4 a 63 caracteres</string>
<string name="fido_pin_title">Introduzca el PIN de su autenticador</string>
<string name="fido_pin_ok">Aceptar</string>
<string name="fido_pin_cancel">Cancelar</string>
<string name="fido_wrong_pin">Se introdujo el PIN incorrecto.</string>
<string name="fido_transport_modify">Cambie cómo se usa la llave de seguridad</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">آغاز کنید</string>
<string name="fido_welcome_privileged_info">%1$s به عنوان یک مرورگر مورد اعتماد برای استفاده از کلید امنیتی شما با %2$s عمل می‌کند.</string>
<string name="fido_welcome_privileged_check">بله، %1$s مرورگر مورد اعتماد من است و باید اجازه استفاده از کلیدهای امنیتی با وبگاه‌های شخص ثالث را داشته باشد.</string>
<string name="fido_biometric_prompt_title">هویت خود را ثابت کنید</string>
<string name="fido_transport_selection_title">انتخاب کنید چگونه از کلید امنیتی خود استفاده کنید</string>
<string name="fido_transport_selection_body">کلیدهای امنیتی با بلوتوث، ان‌اف‌سی و یو‌اس‌بی کار می‌کنند. انتخاب کنید چگونه می‌خواهید از کلید خود استفاده کنید.</string>
<string name="fido_biometric_prompt_body">%1$s نیاز دارد ثابت کند که این شما هستید.</string>
<string name="fido_usb_title">کلید امنیتی یو‌اس‌بی خود را متصل کنید</string>
<string name="fido_nfc_title">کلید امنیتی ان‌اف‌سی خود را متصل کنید</string>
<string name="fido_nfc_prompt_body">کلید خود را به صورت صاف روی پشت دستگاه خود نگه دارید تا زمانی که لرزش آن متوقف شود</string>
<string name="fido_transport_selection_bluetooth">استفاده از کلید امنیتی با بلوتوث</string>
<string name="fido_transport_selection_usb">استفاده از کلید امنیتی با یو‌اس‌بی</string>
<string name="fido_transport_selection_nfc">استفاده از کلید امنیتی با ان‌اف‌سی</string>
<string name="fido_transport_selection_biometric">استفاده از این دستگاه با قفل صفحه</string>
<string name="fido_transport_usb_wait_connect_body">لطفاً کلید امنیتی یو‌اس‌بی خود را متصل کنید.</string>
<string name="fido_pin_title">لطفاً رمز کوتاه احرازکننده هویت خود را وارد کنید</string>
<string name="fido_pin_cancel">رد کردن</string>
<string name="fido_wrong_pin">رمز کوتاه اشتباه وارد شد!</string>
<string name="fido_transport_usb_wait_confirm_body">لطفاً روی حلقه یا دیسک طلایی روی %1$s ضربه بزنید.</string>
<string name="fido_transport_modify">تغییر روش استفاده از کلید امنیتی</string>
<string name="fido_welcome_title">از کلید امنیتی خود با %1$s استفاده کنید</string>
<string name="fido_welcome_body">استفاده از کلید امنیتی با %1$s به محافظت از داده‌های خصوصی شما کمک می‌کند.</string>
<string name="fido_usb_prompt_body">کلید امنیتی خود را به درگاه یو‌اس‌بی متصل کنید یا آن را با کابل یو‌اس‌بی وصل کنید. اگر کلید شما دکمه یا دیسک طلایی دارد، اکنون روی آن ضربه بزنید.</string>
<string name="fido_pin_hint">۴ تا ۶۳ نویسه</string>
<string name="fido_pin_ok">پذیرش</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Gamitin ang iyong security key sa %1$s</string>
<string name="fido_welcome_body">Ang paggamit ng security key sa %1$s ay tinutulungang protektahan ang iyong pribadong data.</string>
<string name="fido_welcome_button_get_started">Magsimula</string>
<string name="fido_welcome_privileged_check">Oo, ang %1$s ay ang aking pinagkakatiwalaang browser at papayagan na gamitin ang mga security key sa mga third-party na website.</string>
<string name="fido_transport_selection_title">Pumili kung paano gamitin ang iyong security key</string>
<string name="fido_biometric_prompt_title">I-verify ang iyong pagkakakilanlan</string>
<string name="fido_biometric_prompt_body">Kailangang i-verify ka ng %1$s.</string>
<string name="fido_usb_title">Konektahin ang iyong USB security key</string>
<string name="fido_usb_prompt_body">Konektahin ang iyong security key sa USB port o konektahin gamit ng USB cable. Kapag may button o gintong disc ang iyong key, i-tap ngayon.</string>
<string name="fido_nfc_prompt_body">Hawakan ng patag ang iyong key sa likod ng iyong device hanggang sa tumigil ito sa pag-vibrate</string>
<string name="fido_transport_selection_bluetooth">Gamitin ang iyong security key gamit ng Bluetooth</string>
<string name="fido_transport_selection_nfc">Gamitin ang iyong security gamit ng NFC</string>
<string name="fido_transport_selection_usb">Gamitin ang security key gamit ng USB</string>
<string name="fido_transport_usb_wait_connect_body">Paki-konekta ng iyong USB security key.</string>
<string name="fido_transport_usb_wait_confirm_body">Paki-tap ang gintong ring o disc sa %1$s.</string>
<string name="fido_welcome_privileged_info">Ang %1$s ay gumaganap bilang pinagkakatiwalaang browser para gamitin ang iyong security key sa %2$s.</string>
<string name="fido_transport_selection_body">Gumagana ang mga security key sa Bluetooth, NFC, at USB. Piliin kung paano mo gustong gamitin ang iyong key.</string>
<string name="fido_nfc_title">Konektahin ang iyong NFC security key</string>
<string name="fido_transport_selection_biometric">Gamitin ang device na ito gamit ng screen lock</string>
<string name="fido_pin_title">Pakilagay ang PIN para sa iyong authenticator</string>
<string name="fido_pin_hint">4 hanggang 63 karakter</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Kanselahin</string>
<string name="fido_wrong_pin">Maling PIN ang inilagay!</string>
<string name="fido_transport_modify">Baguhin kung paano mo gagamitin ang iyong security key</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Utilisez votre clé de sécurité avec %1$s</string>
<string name="fido_pin_title">Merci de saisir le code PIN de votre authentifiant</string>
<string name="fido_pin_hint">4 à 63 caractères</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Annuler</string>
<string name="fido_wrong_pin">Mauvais code PIN saisi!</string>
<string name="fido_welcome_body">Utiliser votre clé de sécurité avec %1$s contribue à protéger vos données privées.</string>
<string name="fido_welcome_button_get_started">Commencer</string>
<string name="fido_welcome_privileged_info">%1$s agit en tant que navigateur de confiance pour utiliser votre clé de sécurité avec %2$s.</string>
<string name="fido_welcome_privileged_check">Oui, %1$s est mon navigateur de confiance et peut être autorisé à utiliser des clés de sécurité avec des sites web tiers.</string>
<string name="fido_transport_selection_title">Choisir comment utiliser votre clé de sécurité</string>
<string name="fido_transport_selection_body">Les clés de sécurité fonctionnent via Bluetooth, NFC ou USB. Choisissez comment utiliser votre clé.</string>
<string name="fido_biometric_prompt_title">Confirmez votre identité</string>
<string name="fido_biometric_prompt_body">%1$s nécessite de vérifier que c\'est bien vous.</string>
<string name="fido_usb_title">Connecter votre clé de sécurité USB</string>
<string name="fido_usb_prompt_body">Connectez votre clé de sécurité à un port USB ou connectez-là avec un câble USB. Si votre clé a un bouton ou un disque doré, appuyez dessus.</string>
<string name="fido_nfc_title">Connecter votre clé de sécurité NFC</string>
<string name="fido_nfc_prompt_body">Tenez votre clé contre l\'arrière de votre appareil jusqu\'à la fin des vibrations</string>
<string name="fido_transport_selection_bluetooth">Utiliser votre clé de sécurité via Bluetooth</string>
<string name="fido_transport_selection_nfc">Utiliser votre clé de sécurité via NFC</string>
<string name="fido_transport_selection_usb">Utiliser votre clé de sécurité via USB</string>
<string name="fido_transport_selection_biometric">Utiliser le verrouillage écran de cet appareil</string>
<string name="fido_transport_usb_wait_connect_body">Merci de connecter votre clé de sécurité USB.</string>
<string name="fido_transport_usb_wait_confirm_body">Merci d\'appuyer sur l\'anneau ou disque doré sur %1$s.</string>
<string name="fido_transport_modify">Changer le fonctionnement de votre clé de sécurité</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Úsáid d\'eochair shlándála le %1$s</string>
<string name="fido_welcome_button_get_started">Cuir tús leis</string>
<string name="fido_transport_selection_nfc">Úsáid eochair shlándála le NFC</string>
<string name="fido_transport_selection_biometric">Úsáid an gléas seo le glas scáileáin</string>
<string name="fido_transport_usb_wait_confirm_body">Tapáil an fáinne órga nó an diosca ar %1$s.</string>
<string name="fido_welcome_body">Má úsáideann tú d\'eochair shlándála le %1$s cuidítear do shonraí príobháideacha a chosaint.</string>
<string name="fido_welcome_privileged_info">Feidhmíonn %1$s mar bhrabhsálaí iontaofa chun d\'eochair shlándála a úsáid le %2$s.</string>
<string name="fido_biometric_prompt_title">Fíoraigh d\'aitheantas</string>
<string name="fido_welcome_privileged_check">Sea, is é %1$s mo bhrabhsálaí iontaofa agus ba cheart go mbeadh cead aige eochracha slándála a úsáid le suíomhanna gréasáin tríú páirtí.</string>
<string name="fido_transport_selection_body">Oibríonn eochracha slándála le Bluetooth, NFC, agus USB. Roghnaigh conas ba mhaith leat d\'eochair a úsáid.</string>
<string name="fido_biometric_prompt_body">Caithfidh %1$s a fhíorú gur tusa atá ann.</string>
<string name="fido_transport_selection_title">Roghnaigh conas d\'eochair shlándála a úsáid</string>
<string name="fido_usb_title">Ceangail d\'eochair shlándála USB</string>
<string name="fido_nfc_title">Ceangail d\'eochair shlándála NFC</string>
<string name="fido_usb_prompt_body">Ceangail d\'eochair shlándála leis an gcalafort USB nó ceangail le cábla USB í. Má tá cnaipe nó diosca óir ar deochair, tapáil anois é.</string>
<string name="fido_nfc_prompt_body">Coinnigh d\'eochair cothrom ar chúl do ghléis go dtí go stopann sé ag creathadh</string>
<string name="fido_transport_selection_bluetooth">Bain úsáid as eochair shlándála le Bluetooth</string>
<string name="fido_transport_usb_wait_connect_body">Ceangail d\'eochair shlándála USB le do thoil.</string>
<string name="fido_transport_selection_usb">Úsáid eochair shlándála le USB</string>
<string name="fido_pin_title">Cuir isteach UAP do fhíordheimhneora</string>
<string name="fido_pin_hint">4 go 63 carachtar</string>
<string name="fido_pin_ok">Ceart go leor</string>
<string name="fido_pin_cancel">Cealaigh</string>
<string name="fido_wrong_pin">UAP mícheart curtha isteach!</string>
<string name="fido_transport_modify">Athraigh conas d\'eochair shlándála a úsáid</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Gunakan kunci keamanan Anda dengan %1$s</string>
<string name="fido_welcome_body">Menggunakan kunci keamanan Anda dengan %1$s membantu melindungi data pribadi Anda.</string>
<string name="fido_welcome_button_get_started">Memulai</string>
<string name="fido_welcome_privileged_info">%1$s berfungsi sebagai browser tepercaya untuk menggunakan kunci keamanan Anda dengan %2$s.</string>
<string name="fido_welcome_privileged_check">Ya, %1$s adalah browser tepercaya saya dan seharusnya diizinkan untuk menggunakan kunci keamanan dengan situs web pihak ketiga.</string>
<string name="fido_transport_selection_title">Pilih cara menggunakan kunci keamanan Anda</string>
<string name="fido_transport_selection_body">Kunci keamanan berfungsi dengan Bluetooth, NFC, dan USB. Pilih cara Anda ingin menggunakan kunci Anda.</string>
<string name="fido_biometric_prompt_title">Verifikasi identitas Anda</string>
<string name="fido_biometric_prompt_body">%1$s perlu memverifikasi bahwa itu Anda.</string>
<string name="fido_usb_title">Hubungkan kunci keamanan USB Anda</string>
<string name="fido_usb_prompt_body">Hubungkan kunci keamanan Anda ke port USB atau hubungkan dengan kabel USB. Jika kunci Anda memiliki tombol atau cakram emas, tekan tombol atau cakram tersebut sekarang.</string>
<string name="fido_nfc_title">Hubungkan kunci keamanan NFC Anda</string>
<string name="fido_nfc_prompt_body">Tempelkan kunci Anda secara datar pada bagian belakang perangkat Anda hingga berhenti bergetar</string>
<string name="fido_transport_selection_bluetooth">Gunakan kunci keamanan dengan Bluetooth</string>
<string name="fido_transport_selection_nfc">Gunakan kunci keamanan dengan NFC</string>
<string name="fido_transport_selection_usb">Gunakan kunci keamanan dengan USB</string>
<string name="fido_transport_selection_biometric">Gunakan perangkat ini dengan kunci layar</string>
<string name="fido_transport_usb_wait_connect_body">Silakan hubungkan kunci keamanan USB Anda.</string>
<string name="fido_transport_usb_wait_confirm_body">Silakan ketuk cincin atau piringan emas pada %1$s.</string>
<string name="fido_pin_title">Silakan masukkan PIN untuk autentikator Anda</string>
<string name="fido_pin_hint">4 hingga 63 karakter</string>
<string name="fido_pin_ok">Oke</string>
<string name="fido_pin_cancel">Batal</string>
<string name="fido_wrong_pin">PIN yang dimasukkan salah!</string>
<string name="fido_transport_modify">Ubah cara penggunaan kunci keamanan Anda</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Notaðu öryggislykilinn þinn með %1$s</string>
<string name="fido_welcome_privileged_info">%1$s virkar sem treystur vafri til að nota öryggislykilinn þinn með %2$s.</string>
<string name="fido_welcome_privileged_check">Já, %1$s er vafrinn sem ég treysti og ætti að vera leyft að nota öryggislykla með utanaðkomandi vefsvæðum.</string>
<string name="fido_nfc_prompt_body">Haltu lyklinum þínum upp við bak tækisins þar til það hættir að titra</string>
<string name="fido_pin_hint">4 til 63 stafir</string>
<string name="fido_welcome_body">Að nota öryggislykilinn þinn með %1$s hjálpar til við að vernda einkagögnin þín.</string>
<string name="fido_welcome_button_get_started">Hefjumst handa</string>
<string name="fido_transport_selection_title">Veldu hvernig á að nota öryggislykilinn þinn</string>
<string name="fido_transport_selection_body">Öryggislyklar virka með Bluetooth, NFC og USB. Veldu hvernig á að nota öryggislykilinn þinn.</string>
<string name="fido_biometric_prompt_title">Sannreyndu auðkennin þín</string>
<string name="fido_biometric_prompt_body">%1$s þarf að sannreyna að þetta sért þú.</string>
<string name="fido_usb_title">Tengdu USB-öryggislykilinn þinn</string>
<string name="fido_usb_prompt_body">Tengdu öryggislykilinn þinn við USB-gátt eða með USB-kapli. Ef minnislykillinn er með hnapp eða gullinn disk, skaltu ýta á hann núna.</string>
<string name="fido_nfc_title">Tengdu NFC-öryggislykilinn þinn</string>
<string name="fido_transport_selection_bluetooth">Nota öryggislykil með Bluetooth</string>
<string name="fido_transport_selection_nfc">Nota öryggislykil með NFC</string>
<string name="fido_transport_selection_usb">Nota öryggislykil með USB</string>
<string name="fido_transport_selection_biometric">Nota þetta tæki með skjálæsingu</string>
<string name="fido_transport_usb_wait_connect_body">Tengdu USB-öryggislykilinn þinn.</string>
<string name="fido_transport_usb_wait_confirm_body">Ýttu á gyllta diskinn eða hringinn á %1$s.</string>
<string name="fido_pin_title">Settu inn PIN-númerið fyrir auðkenningarforritið</string>
<string name="fido_pin_ok">Í lagi</string>
<string name="fido_pin_cancel">Hætta við</string>
<string name="fido_wrong_pin">Rangt PIN-númer sett inn!</string>
<string name="fido_transport_modify">Breyttu því hvernig þú notar öryggislykilinn þinn</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">Inizia</string>
<string name="fido_usb_prompt_body">Collega la tua chiave di sicurezza alla porta USB, o collegala con un cavo USB. Se la tua chiave ha un pulsante o un disco dorato, premilo ora.</string>
<string name="fido_transport_usb_wait_connect_body">Per favore, collega la tua chiave di sicurezza via USB.</string>
<string name="fido_nfc_prompt_body">Tieni appoggiata la tua chiave sul retro del tuo dispositivo fino a quando non smette di vibrare</string>
<string name="fido_welcome_body">Usare la tua chiave di sicurezza con %1$s aiuta a proteggere i tuoi dati personali.</string>
<string name="fido_welcome_title">Usa la tua chiave di sicurezza con %1$s</string>
<string name="fido_transport_selection_biometric">Usa questo dispositivo con un blocco schermo</string>
<string name="fido_welcome_privileged_check">Sì, %1$s è il mio broswer di fiducia e deve avere il permesso di usare le chiavi di sicurezza con siti web di terze parti.</string>
<string name="fido_biometric_prompt_body">%1$s deve verificare che sia tu.</string>
<string name="fido_transport_selection_usb">Usa chiave di sicurezza via USB</string>
<string name="fido_transport_selection_title">Scegli come usare la tua chiave di sicurezza</string>
<string name="fido_biometric_prompt_title">Verifica la tua identità</string>
<string name="fido_usb_title">Collega la tua chiave di sicurezza via USB</string>
<string name="fido_transport_usb_wait_confirm_body">Per favore, premi l\'anello o il disco dorato su %1$s.</string>
<string name="fido_welcome_privileged_info">%1$s agisce come browser fidato per usare la tua chiave di sicurezza con %2$s.</string>
<string name="fido_nfc_title">Collega la tua chiave di sicurezza via NFC</string>
<string name="fido_transport_selection_bluetooth">Usa chiave di sicurezza via Bluetooth</string>
<string name="fido_transport_selection_body">Le chiavi di sicurezza funzionano via Bluetooth, NFC e USB. Scegli come vuoi usare la tua chiave.</string>
<string name="fido_transport_selection_nfc">Usa chiave di sicurezza via NFC</string>
<string name="fido_pin_title">Inserisci il PIN per l\'autenticazione</string>
<string name="fido_pin_hint">Da 4 a 63 caratteri</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Annulla</string>
<string name="fido_wrong_pin">PIN inserito errato!</string>
<string name="fido_transport_modify">Cambia la modalità in cui utilizzi la tua chiave di sicurezza</string>
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_transport_selection_body">セキュリティキーは、Bluetooth、NFC、USBで動作します。キーの使用方法を選択してください。</string>
<string name="fido_nfc_prompt_body">振動が止まるまで、キーをデバイスの背面に平らにかざします</string>
<string name="fido_transport_selection_nfc">NFCでセキュリティキーを使用する</string>
<string name="fido_transport_selection_usb">USBでセキュリティキーを使用する</string>
<string name="fido_pin_hint">4から63文字</string>
<string name="fido_welcome_title">%1$sでセキュリティキーを使用する</string>
<string name="fido_welcome_body">%1$sでセキュリティキーを使用すると、プライベートデータを保護できます。</string>
<string name="fido_welcome_button_get_started">始める</string>
<string name="fido_welcome_privileged_info">%1$sは、%2$sでセキュリティキーを使用する信頼できるブラウザとして機能します。</string>
<string name="fido_welcome_privileged_check">はい、%1$sは私の信頼できるブラウザであり、サードパーティのWebサイトでセキュリティキーを使用することが許可されるべきです。</string>
<string name="fido_transport_selection_title">セキュリティキーの使用方法を選択してください</string>
<string name="fido_biometric_prompt_body">%1$sはあなたであることを確認する必要があります。</string>
<string name="fido_usb_title">USBセキュリティキーを接続する</string>
<string name="fido_usb_prompt_body">セキュリティキーをUSBポートに接続するか、USBケーブルで接続します。キーにボタンやゴールドディスクがある場合は、今すぐタップしてください。</string>
<string name="fido_nfc_title">NFCセキュリティキーを接続する</string>
<string name="fido_transport_selection_bluetooth">Bluetoothでセキュリティキーを使用する</string>
<string name="fido_transport_selection_biometric">このデバイスでスクリーンロックを使用してください</string>
<string name="fido_transport_usb_wait_connect_body">USBセキュリティキーを接続してください。</string>
</resources>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">%1$s와 보안 키를 사용해보세요</string>
<string name="fido_welcome_body">%1$s와 보안 키를 사용하면 개인 데이터를 보호할 수 있습니다.</string>
<string name="fido_welcome_button_get_started">시작</string>
<string name="fido_welcome_privileged_info">%1$s는 %2$s와 함께 보안 키를 사용하는 신뢰된 브라우저 역할을 합니다.</string>
<string name="fido_welcome_privileged_check">네, %1$s는 신뢰할 수 있는 브라우저이며 타사 웹사이트에서 보안 키를 사용할 수 있게 허용합니다.</string>
<string name="fido_transport_selection_title">보안 키 사용 방법 선택</string>
<string name="fido_transport_selection_body">보안 키는 블루투스, NFC, USB로 작동합니다. 키를 어떻게 사용할지 선택하세요.</string>
<string name="fido_biometric_prompt_title">본인 확인</string>
<string name="fido_biometric_prompt_body">%1$s은 본인 확인이 필요합니다.</string>
<string name="fido_transport_selection_bluetooth">블루투스로 보안 키 사용</string>
<string name="fido_pin_cancel">취소</string>
<string name="fido_usb_title">USB 보안 키 연결</string>
<string name="fido_usb_prompt_body">보안 키를 USB 포트에 연결하거나 USB 케이블로 연결하세요. 키에 버튼이나 금색 디스크가 있는 경우, 지금 눌러주세요.</string>
<string name="fido_nfc_title">NFC 보안 키 연결</string>
<string name="fido_nfc_prompt_body">진동이 멈출 때까지 키를 기기 뒷면에 대주세요</string>
<string name="fido_transport_selection_nfc">NFC로 보안 키 사용</string>
<string name="fido_transport_selection_usb">USB로 보안 키 사용</string>
<string name="fido_transport_selection_biometric">화면 잠금으로 이 기기 사용</string>
<string name="fido_transport_usb_wait_connect_body">USB 보안 키를 연결해 주세요.</string>
<string name="fido_transport_usb_wait_confirm_body">%1$s에 있는 금색 링 또는 디스크를 눌러주세요.</string>
<string name="fido_pin_title">인증기의 PIN을 입력해주세요</string>
<string name="fido_pin_hint">4~63자</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">%1$s-നൊപ്പം നിങ്ങളുടെ സുരക്ഷാ കീ ഉപയോഗിക്കുക</string>
<string name="fido_welcome_body">%1$s ഉപയോഗിച്ച് നിങ്ങളുടെ സുരക്ഷാ കീ ഉപയോഗിക്കുന്നത് നിങ്ങളുടെ സ്വകാര്യ ഡാറ്റ പരിരക്ഷിക്കാൻ സഹായിക്കുന്നു.</string>
<string name="fido_welcome_button_get_started">ആരംഭിക്കുക</string>
<string name="fido_welcome_privileged_info">%2$s-നൊപ്പം നിങ്ങളുടെ സുരക്ഷാ കീ ഉപയോഗിക്കുന്നതിന് %1$s ഒരു വിശ്വസനീയ ബ്രൗസറായി പ്രവർത്തിക്കുന്നു.</string>
<string name="fido_welcome_privileged_check">അതെ, %1$s എന്റെ വിശ്വസനീയ ബ്രൗസറാണ്, മൂന്നാം കക്ഷി വെബ്‌സൈറ്റുകളിൽ സുരക്ഷാ കീകൾ ഉപയോഗിക്കാൻ അതിനെ അനുവദിക്കണം.</string>
<string name="fido_transport_selection_title">നിങ്ങളുടെ സുരക്ഷാ കീ എങ്ങനെ ഉപയോഗിക്കണമെന്ന് തിരഞ്ഞെടുക്കുക</string>
<string name="fido_transport_selection_body">സുരക്ഷാ കീകൾ ബ്ലൂടൂത്ത് , എൻ എഫ് സി , യു എസ് ബി എന്നിവയിൽ പ്രവർത്തിക്കുന്നു. നിങ്ങളുടെ കീ എങ്ങനെ ഉപയോഗിക്കണമെന്ന് തിരഞ്ഞെടുക്കുക.</string>
<string name="fido_biometric_prompt_title">നിങ്ങളുടെ ഐഡന്റിറ്റി പരിശോധിക്കുക</string>
<string name="fido_biometric_prompt_body">%1$s-ന് ഇത് നിങ്ങളാണെന്ന് സ്ഥിരീകരിക്കേണ്ടതുണ്ട്.</string>
<string name="fido_usb_title">നിങ്ങളുടെ USB സുരക്ഷാ കീ ബന്ധിപ്പിക്കുക</string>
<string name="fido_usb_prompt_body">നിങ്ങളുടെ സുരക്ഷാ കീ USB പോർട്ടുമായി ബന്ധിപ്പിക്കുക അല്ലെങ്കിൽ ഒരു USB കേബിൾ ഉപയോഗിച്ച് ബന്ധിപ്പിക്കുക. നിങ്ങളുടെ കീയിൽ ഒരു ബട്ടണോ സ്വർണ്ണ ഡിസ്കോ ഉണ്ടെങ്കിൽ, ഇപ്പോൾ അതിൽ ടാപ്പ് ചെയ്യുക.</string>
<string name="fido_nfc_title">നിങ്ങളുടെ NFC സുരക്ഷാ കീ ബന്ധിപ്പിക്കുക</string>
<string name="fido_nfc_prompt_body">വൈബ്രേറ്റ് ചെയ്യുന്നത് നിർത്തുന്നത് വരെ നിങ്ങളുടെ ഉപകരണത്തിന്റെ പിൻഭാഗത്ത് കീ നേരെ അമർത്തിപ്പിടിക്കുക</string>
<string name="fido_transport_selection_bluetooth">ബ്ലൂടൂത്തിനൊപ്പം സുരക്ഷാ കീ ഉപയോഗിക്കുക</string>
<string name="fido_transport_selection_nfc">NFC-യിൽ സുരക്ഷാ കീ ഉപയോഗിക്കുക</string>
<string name="fido_transport_selection_usb">USB-യിൽ സുരക്ഷാ കീ ഉപയോഗിക്കുക</string>
<string name="fido_transport_selection_biometric">സ്ക്രീൻ ലോക്ക് ഉപയോഗിച്ച് ഈ ഉപകരണം ഉപയോഗിക്കുക</string>
<string name="fido_transport_usb_wait_connect_body">നിങ്ങളുടെ USB സുരക്ഷാ കീ ബന്ധിപ്പിക്കുക.</string>
<string name="fido_transport_usb_wait_confirm_body">%1$s-ലെ സ്വർണ്ണ മോതിരം അല്ലെങ്കിൽ ഡിസ്ക് ടാപ്പ് ചെയ്യുക.</string>
<string name="fido_pin_title">നിങ്ങളുടെ ഓതന്റിക്കേറ്ററിന്റെ പിൻ നൽകുക</string>
<string name="fido_pin_hint">4 മുതൽ 63 വരെ പ്രതീകങ്ങൾ</string>
<string name="fido_pin_ok">ശരി</string>
<string name="fido_pin_cancel">റദ്ദാക്കുക</string>
<string name="fido_wrong_pin">തെറ്റായ പിൻ നൽകി!</string>
<string name="fido_transport_modify">നിങ്ങളുടെ സുരക്ഷാ കീ ഉപയോഗിക്കുന്ന രീതി മാറ്റുക</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">Begynn</string>
<string name="fido_welcome_title">Bruk sikkerhetsnøkkelen din med %1$s</string>
<string name="fido_welcome_body">Å bruke sikkerhetsnøkkelen din med %1$s hjelper med å beskytte de private dataene dine.</string>
<string name="fido_welcome_privileged_info">%1$s fungerer som en troverdig nettleser for å bruke sikkerhetsnøkkelen din med %2$s.</string>
<string name="fido_welcome_privileged_check">Ja, %1$s er en nettleser jeg stoler på og burde få tillatelse til å bruke sikkerhetsnøkler med tredjepartsnettsider.</string>
<string name="fido_transport_selection_title">Velg hvordan du vil bruke sikkerhetsnøkkelen din</string>
<string name="fido_transport_selection_body">Sikkerhetsnøkler fungerer over Bluetooth, NFC og USB. Velg hvordan du vil bruke nøkkelen din.</string>
<string name="fido_biometric_prompt_title">Bekreft identiteten din</string>
<string name="fido_biometric_prompt_body">%1$s må bekrefte at det er deg.</string>
<string name="fido_usb_title">Koble til sikkerhetsnøkkelen din over USB</string>
<string name="fido_usb_prompt_body">Kobl sikkerhetsnøkkelen din til en USB port eller gjennom en USB-kabel. Hvis nøkkelen din er en knapp eller en gullfarget disk, trykk på den nå.</string>
<string name="fido_nfc_title">Koble til sikkerhetsnøkkelen din over NFC</string>
<string name="fido_nfc_prompt_body">Hold nøkkelen din flatt over baksiden av enheten din til den stopper å vibrere</string>
<string name="fido_transport_selection_bluetooth">Koble til sikkerhetsnøkkelen din med Bluetooth</string>
<string name="fido_transport_selection_nfc">Bruk sikkerhetsnøkkel med NFC</string>
<string name="fido_transport_selection_usb">Bruk sikkerhetsnøkkel med USB</string>
<string name="fido_transport_selection_biometric">Bruk denne enheten med skjermlås</string>
<string name="fido_transport_usb_wait_connect_body">Koble til USB-sikkerhetsnøkkelen din.</string>
<string name="fido_transport_usb_wait_confirm_body">Trykk på den gyldne ringen eller disken på %1$s.</string>
<string name="fido_pin_title">Tast inn PIN-koden din for å autentiseres</string>
<string name="fido_pin_hint">4 til 63 tegn</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Avbryt</string>
<string name="fido_wrong_pin">Feil PIN-kode!</string>
<string name="fido_transport_modify">Endre hvordan du vil bruke sikkerhetsnøkkelen din</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">Rozpocznij</string>
<string name="fido_usb_prompt_body">Wsuń klucz bezpieczeństwa do portu USB lub połącz się z nim przez kabel USB. Jeżeli twój klucz ma przycisk lub złoty dysk, teraz go dotknij.</string>
<string name="fido_transport_usb_wait_connect_body">Podłącz swój klucz U2F przez USB.</string>
<string name="fido_nfc_prompt_body">Przyłóż swój klucz bezpieczeństwa ściśle do tyłu urządzenia i trzymaj, aż twoje urządzenie przestanie wibrować</string>
<string name="fido_welcome_body">Używanie swojego klucza bezpieczeństwa z %1$s pomaga chronić twoje prywatne dane.</string>
<string name="fido_welcome_title">Używaj swojego klucza U2F z %1$s</string>
<string name="fido_transport_selection_biometric">Korzystaj z tego urządzenia przez blokadę ekranu</string>
<string name="fido_welcome_privileged_check">Tak, %1$s jest moją zaufaną przeglądarką i powinna mieć dostęp do korzystania z kluczy bezpieczeństwa przez witryny firm trzecich.</string>
<string name="fido_biometric_prompt_body">%1$s chce potwierdzić, że to Ty.</string>
<string name="fido_transport_selection_usb">Korzystaj z klucza przez USB</string>
<string name="fido_transport_selection_title">Wybierz, jak korzystać ze swojego klucza bezpieczeństwa</string>
<string name="fido_biometric_prompt_title">Potwierdź swoją tożsamość</string>
<string name="fido_usb_title">Połącz ze swoim kluczem przez USB</string>
<string name="fido_transport_usb_wait_confirm_body">Dotknij złotego pierścienia lub dysku na %1$s.</string>
<string name="fido_welcome_privileged_info">%1$s powinna funkcjonować jako bezpieczna przeglądarka do używania własnego klucza bezpieczeństwa z %2$s.</string>
<string name="fido_nfc_title">Połącz ze swoim kluczem przez NFC</string>
<string name="fido_transport_selection_bluetooth">Korzystaj z klucza przez Bluetooth</string>
<string name="fido_transport_selection_body">Klucze bezpieczeństwa mogą działać przez Bluetooth, NFC i USB. Wybierz jak chcesz korzystać ze swojego klucza.</string>
<string name="fido_transport_selection_nfc">Korzystaj z klucza przez NFC</string>
<string name="fido_wrong_pin">Wprowadzono błędny kod PIN!</string>
<string name="fido_pin_title">Wprowadź kod PIN urządzenia uwierzytelniającego</string>
<string name="fido_pin_hint">Od 4 do 63 znaków</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Anuluj</string>
<string name="fido_transport_modify">Zmiana sposobu korzystania z klucza zabezpieczeń</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_body">Usar sua chave de segurança com %1$s ajuda a proteger seus dados.</string>
<string name="fido_welcome_button_get_started">Iniciar</string>
<string name="fido_welcome_privileged_info">%1$s age como um navegador confiado para utilizar sua chave de segurança com %2$s.</string>
<string name="fido_transport_selection_title">Escolha como usar sua chave de segurança</string>
<string name="fido_transport_selection_bluetooth">Usar chave de segurança com Bluetooth</string>
<string name="fido_welcome_privileged_check">Sim, %1$s é meu navegador confiado e deve ser permitido que use chaves de segurança com sites da web de terceiros.</string>
<string name="fido_usb_title">Conecte sua chave de segurança USB</string>
<string name="fido_transport_selection_body">Chaves de segurança funcionam com Bluetooth, NFC e USB. Escolha como quer usar sua chave.</string>
<string name="fido_nfc_prompt_body">Segure sua chave de segurança atrás do seu dispositivo até que ele pare de vibrar</string>
<string name="fido_biometric_prompt_title">Verifique sua identidade</string>
<string name="fido_biometric_prompt_body">%1$s precisa verificar que é você.</string>
<string name="fido_usb_prompt_body">Conecte sua chave de segurança na porta USB ou com um cabo USB. Se sua chave tem um botão ou disco de ouro, clique nele agora.</string>
<string name="fido_nfc_title">Conecte sua chave de segurança NFC</string>
<string name="fido_welcome_title">Use sua chave de segurança com %1$s</string>
<string name="fido_transport_selection_nfc">Usar chave de segurança com NFC</string>
<string name="fido_transport_selection_usb">Usar chave de segurança com USB</string>
<string name="fido_transport_selection_biometric">Usar este dispositivo com o bloqueio de tela</string>
<string name="fido_transport_usb_wait_connect_body">Por favor, conecte sua chave de segurança USB.</string>
<string name="fido_transport_usb_wait_confirm_body">Por favor, clique no disco ou anel de ouro em %1$s.</string>
<string name="fido_pin_title">Por favor, digite o PIN do seu autenticador</string>
<string name="fido_pin_hint">de 4 à 63 caracteres</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Cancelar</string>
<string name="fido_wrong_pin">O PIN digitado está incorreto!</string>
<string name="fido_transport_modify">Mudar o modo de uso da chave de segurança</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_title">Use a sua chave de segurança com %1$s</string>
<string name="fido_welcome_body">Usar a sua chave de segurança com %1$s ajuda a proteger os seus dados.</string>
<string name="fido_welcome_button_get_started">Iniciar</string>
<string name="fido_welcome_privileged_info">%1$s age como um navegador confiado para utilizar a sua chave de segurança com %2$s.</string>
<string name="fido_welcome_privileged_check">Sim, %1$s é o meu navegador confiado e deve ser permitido que use chaves de segurança com sites da web de terceiros.</string>
<string name="fido_transport_selection_title">Escolha como usar a sua chave de segurança</string>
<string name="fido_transport_selection_body">Chaves de segurança funcionam com Bluetooth, NFC e USB. Escolha como quer usar a sua chave.</string>
<string name="fido_biometric_prompt_title">Verifique a sua identidade</string>
<string name="fido_biometric_prompt_body">%1$s precisa verificar que é você.</string>
<string name="fido_usb_title">Conecte a sua chave de segurança USB</string>
<string name="fido_usb_prompt_body">Conecte a sua chave de segurança na porta USB ou com um cabo USB. Se a sua chave tem um botão ou disco de ouro, clique nele agora.</string>
<string name="fido_nfc_title">Conecte a sua chave de segurança NFC</string>
<string name="fido_nfc_prompt_body">Segure a sua chave de segurança atrás do seu dispositivo até que ele pare de vibrar</string>
<string name="fido_transport_selection_bluetooth">Usar chave de segurança com Bluetooth</string>
<string name="fido_transport_selection_nfc">Usar chave de segurança com NFC</string>
<string name="fido_transport_selection_usb">Usar chave de segurança com USB</string>
<string name="fido_transport_selection_biometric">Usar este dispositivo com o bloqueio de ecrã</string>
<string name="fido_transport_usb_wait_connect_body">Por favor, conecte a sua chave de segurança USB.</string>
<string name="fido_transport_usb_wait_confirm_body">Por favor, clique no disco ou anel de ouro em %1$s.</string>
<string name="fido_pin_title">Por favor, digite o PIN do seu autenticador</string>
<string name="fido_pin_hint">de 4 à 63 caracteres</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_pin_cancel">Cancelar</string>
<string name="fido_wrong_pin">O PIN digitado está incorreto!</string>
<string name="fido_transport_modify">Mudar o modo de uso da chave de segurança</string>
</resources>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fido_welcome_button_get_started">Să începem</string>
<string name="fido_usb_prompt_body">Conectează cheia de securitate la portul USB sau printr-un cablu USB. Dacă cheia are un buton sau un disc auriu, atinge-l acum.</string>
<string name="fido_transport_usb_wait_connect_body">Conectează cheia de securitate USB.</string>
<string name="fido_nfc_prompt_body">Ține cheia pe partea din spate a dispozitivului până când nu mai vibrează</string>
<string name="fido_welcome_body">Folosirea cheii de securitate cu %1$s te ajută să protejezi datele private.</string>
<string name="fido_welcome_title">Utilizează cheia de securitate pentru %1$s</string>
<string name="fido_transport_selection_biometric">Utilizează acest dispozitiv cu blocarea ecranului</string>
<string name="fido_welcome_privileged_check">Da, %1$s este browser-ul meu de încredere și ar trebui să aibă permisiunea de a utiliza chei de securitate cu site-uri web terțe.</string>
<string name="fido_biometric_prompt_body">%1$s trebuie să verifice dacă ești tu.</string>
<string name="fido_transport_selection_usb">Utilizează cheia de securitate cu USB</string>
<string name="fido_transport_selection_title">Alege cum să utilizezi cheia de securitate</string>
<string name="fido_biometric_prompt_title">Verifică-ți identitatea</string>
<string name="fido_usb_title">Conectează cheia de securitate USB</string>
<string name="fido_transport_usb_wait_confirm_body">Atinge inelul sau discul de aur de pe %1$s.</string>
<string name="fido_welcome_privileged_info">%1$s acționează ca un browser de încredere pentru a-ți folosi cheia de securitate %2$s.</string>
<string name="fido_nfc_title">Conectează cheia de securitate NFC</string>
<string name="fido_transport_selection_bluetooth">Utilizează cheia de securitate cu Bluetooth</string>
<string name="fido_transport_selection_body">Cheile de securitate funcționează cu Bluetooth, NFC și USB. Alege cum dorești să utilizezi cheia.</string>
<string name="fido_transport_selection_nfc">Utilizează cheia de securitate cu NFC</string>
<string name="fido_pin_title">Introdu codul PIN pentru autentificatorul tău</string>
<string name="fido_pin_hint">De la 4 la 63 de caractere</string>
<string name="fido_pin_cancel">Anulează</string>
<string name="fido_pin_ok">OK</string>
<string name="fido_wrong_pin">PIN greșit!</string>
<string name="fido_transport_modify">Schimbă cum să utilizezi cheia de securitate</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more