Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class BeginSignInRequest extends AbstractSafeParcelable {
private final PasskeysRequestOptions passkeysRequestOptions;
@Field(value = 7, getterName = "getPasskeyJsonRequestOptions")
private final PasskeyJsonRequestOptions passkeyJsonRequestOptions;
@Field(value = 8, getterName = "isPreferImmediatelyAvailableCredentials")
private final boolean preferImmediatelyAvailableCredentials;

@NonNull
@Override
Expand All @@ -55,18 +57,20 @@ public String toString() {
.field("theme", theme)
.field("PasskeysRequestOptions", passkeysRequestOptions)
.field("PasskeyJsonRequestOptions", passkeyJsonRequestOptions)
.field("preferImmediatelyAvailableCredentials", preferImmediatelyAvailableCredentials)
.end();
}

@Constructor
BeginSignInRequest(@Param(1) PasswordRequestOptions passwordRequestOptions, @Param(2) GoogleIdTokenRequestOptions googleIdTokenRequestOptions, @Param(3) String sessionId, @Param(4) boolean autoSelectEnabled, @Param(5) int theme, @Param(6) PasskeysRequestOptions passkeysRequestOptions, @Param(7) PasskeyJsonRequestOptions passkeyJsonRequestOptions) {
BeginSignInRequest(@Param(1) PasswordRequestOptions passwordRequestOptions, @Param(2) GoogleIdTokenRequestOptions googleIdTokenRequestOptions, @Param(3) String sessionId, @Param(4) boolean autoSelectEnabled, @Param(5) int theme, @Param(6) PasskeysRequestOptions passkeysRequestOptions, @Param(7) PasskeyJsonRequestOptions passkeyJsonRequestOptions, @Param(8) boolean preferImmediatelyAvailableCredentials) {
this.passwordRequestOptions = passwordRequestOptions;
this.googleIdTokenRequestOptions = googleIdTokenRequestOptions;
this.sessionId = sessionId;
this.autoSelectEnabled = autoSelectEnabled;
this.theme = theme;
this.passkeysRequestOptions = passkeysRequestOptions;
this.passkeyJsonRequestOptions = passkeyJsonRequestOptions;
this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials;
}

@NonNull
Expand Down Expand Up @@ -107,6 +111,10 @@ public boolean isAutoSelectEnabled() {
return autoSelectEnabled;
}

public boolean isPreferImmediatelyAvailableCredentials() {
return preferImmediatelyAvailableCredentials;
}

public static class Builder {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME
import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS
import org.microg.gms.auth.signin.performSignOut
import org.microg.gms.common.GmsService
import org.microg.gms.fido.core.Database
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_SOURCE
Expand Down Expand Up @@ -94,6 +95,7 @@ class IdentitySignInServiceImpl(private val context: Context, private val client
fun <T> JSONArray.map(fn: (JSONObject) -> T): List<T> = (0 until length()).map { fn(getJSONObject(it)) }
fun <T> JSONArray.map(fn: (String) -> T): List<T> = (0 until length()).map { fn(getString(it)) }
val json = JSONObject(request.passkeyJsonRequestOptions.requestJson)
val rpId = json.getString("rpId")
val options = PublicKeyCredentialRequestOptions.Builder()
.setAllowList(json.getArrayOrNull("allowCredentials")?.let { allowCredentials -> allowCredentials.map { credential: JSONObject ->
PublicKeyCredentialDescriptor(
Expand All @@ -106,9 +108,14 @@ class IdentitySignInServiceImpl(private val context: Context, private val client
} })
.setChallenge(Base64.decode(json.getString("challenge"), Base64.URL_SAFE))
.setRequireUserVerification(json.optString("userVerification").takeIf { it.isNotBlank() }?.let { UserVerificationRequirement.fromString(it) })
.setRpId(json.getString("rpId"))
.setRpId(rpId)
.setTimeoutSeconds(json.optDouble("timeout", -1.0).takeIf { it > 0 })
.build()
if (request.isPreferImmediatelyAvailableCredentials && Database(context).getKnownRegistrationInfo(rpId).isEmpty()) {
Log.d(TAG, "need available Credential")
callback.onResult(Status.CANCELED, null)
return
}
val bundle = bundleOf(
KEY_SERVICE to GmsService.IDENTITY_SIGN_IN.SERVICE_ID,
KEY_SOURCE to "app",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ class KeyRetrievalServiceImpl(val context: Context) : IKeyRetrievalService.Stub(
callback: ISharedKeyCallback?, accountName: String?, metadata: ApiMetadata?
) {
Log.d(TAG, "Not implemented getKeyMaterial accountName:$accountName metadata:$metadata")
callback?.onResult(Status.SUCCESS, emptyArray<SharedKey>())
callback?.onResult(Status.INTERNAL_ERROR, emptyArray<SharedKey>())
}

override fun setKeyMaterial(
callback: IKeyRetrievalCallback?, accountName: String?, keys: Array<out SharedKey?>?, metadata: ApiMetadata?
) {
Log.d(TAG, "Not implemented setKeyMaterial accountName:$accountName keys:$keys metadata:$metadata")
callback?.onResult(Status.SUCCESS)
callback?.onResult(Status.INTERNAL_ERROR)
}

override fun getRecoveredSecurityDomains(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ 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_IGNORE
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) {

Expand All @@ -31,6 +33,23 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE
}
}

fun getKnownRegistrationInfo(rpId: String) = readableDatabase.use {
val cursor = it.query(
TABLE_KNOWN_REGISTRATIONS, arrayOf(COLUMN_CREDENTIAL_ID, COLUMN_REGISTER_USER, COLUMN_TRANSPORT), "$COLUMN_RP_ID=?", arrayOf(rpId), null, null, null
)
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)
Expand All @@ -39,13 +58,33 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE
}, CONFLICT_REPLACE)
}

fun insertKnownRegistration(rpId: String, credentialId: String, transport: Transport) = writableDatabase.use {
it.insertWithOnConflict(TABLE_KNOWN_REGISTRATIONS, null, ContentValues().apply {
put(COLUMN_RP_ID, rpId)
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())
}, CONFLICT_REPLACE)
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) {
Expand All @@ -59,10 +98,13 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE
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 = 2
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"
Expand All @@ -71,6 +113,7 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE
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"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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
Expand All @@ -30,6 +31,7 @@ class RequestHandlingException(val errorCode: ErrorCode, message: String? = null
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
Expand Down Expand Up @@ -71,6 +73,12 @@ val RequestOptions.rpId: String
SIGN -> signOptions.rpId
}

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

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

Expand Down Expand Up @@ -155,19 +163,15 @@ private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: Str
}

suspend fun RequestOptions.checkIsValid(context: Context, facetId: String, packageName: String?) {
if (type == SIGN) {
if (signOptions.allowList.isNullOrEmpty()) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "Request doesn't have a valid list of allowed credentials.")
}
}
if (facetId.startsWith("https://")) {
if (topDomainOf(Uri.parse(facetId).host) != topDomainOf(rpId)) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId")
}
// FIXME: Standard suggests doing additional checks, but this is already sensible enough
} else if (facetId.startsWith("android:apk-key-hash:") && packageName != null) {
val sha256FacetId = getAltFacetId(context, packageName, facetId) ?:
throw RequestHandlingException(NOT_ALLOWED_ERR, "Can't resolve $facetId to SHA-256 Facet")
val sha256FacetId = getAltFacetId(context, packageName, facetId)?.ifEmpty {
getAltFacetId(context, packageName, getApkKeyHashFacetId(context, packageName))
} ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Can't resolve $facetId to SHA-256 Facet")
if (!isAssetLinked(context, rpId, sha256FacetId, packageName)) {
throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $sha256FacetId")
}
Expand Down Expand Up @@ -213,21 +217,18 @@ fun getApplicationName(context: Context, options: RequestOptions, callingPackage
}

fun getApkKeyHashFacetId(context: Context, packageName: String): String {
val digest = context.packageManager.getFirstSignatureDigest(packageName, "SHA1")
// Default: SHA-256
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)}"
Comment on lines +220 to 223
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems wrong. It should be either android:apk-key-hash:<sha-1> or android:apk-key-hash-sha256:<sha-256>, but not a mix.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed this, but in the fido specification, the pr server recognizes SHA-256

}

fun getAltFacetId(context: Context, packageName: String, facetId: String): String? {
val firstSignature = context.packageManager.getSignatures(packageName).firstOrNull()
?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName")
val sha256BASE64 = firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)
return when (facetId) {
"android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}" -> {
"android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}"
}
"android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}" -> {
"android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}"
}
"android:apk-key-hash:$sha256BASE64" -> "android:apk-key-hash-sha256:$sha256BASE64"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you need this here, because you wrongly created it above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed sha-1 related.

else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

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

Expand All @@ -16,7 +18,15 @@ class CredentialId(val type: Byte, val data: ByteArray, val rpId: String, val pu
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
open val isSupported: Boolean
get() = false

open suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean = false, pin: String? = null): AuthenticatorResponse =
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan
}


override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse {
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
Expand Down
Loading