Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
43
play-services-fido/build.gradle
Normal file
43
play-services-fido/build.gradle
Normal 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')
|
||||
}
|
||||
73
play-services-fido/core/build.gradle
Normal file
73
play-services-fido/core/build.gradle
Normal 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'
|
||||
48
play-services-fido/core/src/main/AndroidManifest.xml
Normal file
48
play-services-fido/core/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) })
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)})"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()"
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
28
play-services-fido/core/src/main/res/values-ar/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-ar/strings.xml
Normal 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>
|
||||
22
play-services-fido/core/src/main/res/values-ast/strings.xml
Normal file
22
play-services-fido/core/src/main/res/values-ast/strings.xml
Normal 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>
|
||||
27
play-services-fido/core/src/main/res/values-az/strings.xml
Normal file
27
play-services-fido/core/src/main/res/values-az/strings.xml
Normal 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>
|
||||
30
play-services-fido/core/src/main/res/values-be/strings.xml
Normal file
30
play-services-fido/core/src/main/res/values-be/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-cs/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-cs/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-de/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-de/strings.xml
Normal 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 geht’s</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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
28
play-services-fido/core/src/main/res/values-es/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-es/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-fa/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-fa/strings.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
28
play-services-fido/core/src/main/res/values-fil/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-fil/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-fr/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-fr/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-ga/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-ga/strings.xml
Normal 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 d’eochair, 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>
|
||||
28
play-services-fido/core/src/main/res/values-in/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-in/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-is/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-is/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-it/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-it/strings.xml
Normal 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>
|
||||
21
play-services-fido/core/src/main/res/values-ja/strings.xml
Normal file
21
play-services-fido/core/src/main/res/values-ja/strings.xml
Normal 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>
|
||||
25
play-services-fido/core/src/main/res/values-ko/strings.xml
Normal file
25
play-services-fido/core/src/main/res/values-ko/strings.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
28
play-services-fido/core/src/main/res/values-ml/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-ml/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
28
play-services-fido/core/src/main/res/values-pl/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-pl/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
28
play-services-fido/core/src/main/res/values-pt/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-pt/strings.xml
Normal 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>
|
||||
28
play-services-fido/core/src/main/res/values-ro/strings.xml
Normal file
28
play-services-fido/core/src/main/res/values-ro/strings.xml
Normal 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
Loading…
Add table
Add a link
Reference in a new issue