Skip to content

Commit

Permalink
Implement "txAuthSimple" extension
Browse files Browse the repository at this point in the history
  • Loading branch information
fmeum committed Dec 17, 2019
1 parent 8bb6e14 commit 9ba054c
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 21 deletions.
16 changes: 16 additions & 0 deletions authenticator/src/main/java/me/henneke/wearauthn/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,19 @@ fun String.escapeHtml(): String = TextUtils.htmlEncode(this)

fun String.truncate(targetLength: Int): String =
if (length > targetLength) this.take(targetLength - 1) + "" else this

fun String.breakAt(lineBreaks: List<Int>): String? {
if (this == "")
return ""
val lineRanges = (listOf(0) + lineBreaks + listOf(length)).zipWithNext()
if (!lineRanges.all { (start, end) -> start < end })
return null

return lineRanges.map { (start, end) ->
val line = substring(start until end)
if (line.last() == '\n' || end == length)
line
else
line + '\n'
}.joinToString(separator = "")
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import kotlinx.coroutines.*
import me.henneke.wearauthn.base64
import me.henneke.wearauthn.escapeHtml
import me.henneke.wearauthn.fido.context.AuthenticatorAction.*
import me.henneke.wearauthn.fido.ctap2.*
import me.henneke.wearauthn.fido.ctap2.AttestationType
import me.henneke.wearauthn.fido.ctap2.CTAP_ERR
import me.henneke.wearauthn.fido.ctap2.CborValue
import me.henneke.wearauthn.fido.ctap2.CtapError.OperationDenied
import me.henneke.wearauthn.fido.ctap2.CtapError.Other
import me.henneke.wearauthn.fido.u2f.resolveAppIdHash
Expand Down Expand Up @@ -187,7 +189,8 @@ enum class AuthenticatorSpecialStatus {
abstract class AuthenticatorContext(val isHidTransport: Boolean) {
abstract fun notifyUser(info: RequestInfo)
abstract fun handleSpecialStatus(specialStatus: AuthenticatorSpecialStatus)
abstract suspend fun confirmWithUser(info: RequestInfo): Boolean
abstract suspend fun confirmRequestWithUser(info: RequestInfo): Boolean
abstract suspend fun confirmTransactionWithUser(rpId: String, prompt: String): String?

// We use cached credentials only over NFC, where low latency responses are very important
private val useCachedCredential = !isHidTransport
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.henneke.wearauthn.fido.ctap2

import android.util.Log
import kotlinx.coroutines.delay
import me.henneke.wearauthn.*
import me.henneke.wearauthn.fido.context.*
import me.henneke.wearauthn.fido.context.AuthenticatorAction.*
Expand Down Expand Up @@ -112,7 +113,7 @@ object Authenticator {
"Received a Chrome GetTouchRequest, replying with dummy response after confirmation"
)
val requestInfo = Ctap2RequestInfo(REQUIREMENTS_NOT_MET_CHROME, rpId)
val showErrorInBrowser = context.confirmWithUser(requestInfo)
val showErrorInBrowser = context.confirmRequestWithUser(requestInfo)
if (showErrorInBrowser)
return DUMMY_MAKE_CREDENTIAL_RESPONSE
else
Expand All @@ -130,7 +131,7 @@ object Authenticator {
rpId = rpId,
rpName = rpName
)
val revealRegistration = context.confirmWithUser(requestInfo)
val revealRegistration = context.confirmRequestWithUser(requestInfo)
if (revealRegistration)
CTAP_ERR(CredentialExcluded)
else
Expand Down Expand Up @@ -166,7 +167,11 @@ object Authenticator {

// Step 4
// We only validate the extension inputs here, the actual processing is done later.
val activeExtensions = parseExtensionInputs(extensions, REGISTER)
val activeExtensions = parseExtensionInputs(
extensions = extensions,
action = REGISTER,
canUseDisplay = context.isHidTransport
)

// Step 7
// We do not support any PIN protocols
Expand All @@ -184,7 +189,7 @@ object Authenticator {
requiresUserVerification = requireUserVerification
)

if (!context.confirmWithUser(requestInfo))
if (!context.confirmRequestWithUser(requestInfo))
CTAP_ERR(OperationDenied)

if (requireUserVerification && !context.verifyUser())
Expand Down Expand Up @@ -316,12 +321,20 @@ object Authenticator {

// Step 6
// We only validate the extension inputs here, the actual processing is done later.
val activeExtensions = parseExtensionInputs(extensions, AUTHENTICATE)
val activeExtensions = parseExtensionInputs(
extensions = extensions,
action = AUTHENTICATE,
canUseDisplay = context.isHidTransport
).toMutableMap()

// hmac-secret requires user presence, but the spec is not clear on whether this has to be
// obtained separately
if (activeExtensions.containsKey(Extension.HmacSecret))
requireUserPresence = true
if (activeExtensions.containsKey(Extension.TxAuthSimple)) {
if (!requireUserPresence && !requireUserVerification)
requireUserPresence = true
}

// Step 1
val useResidentKey = allowList == null
Expand Down Expand Up @@ -361,6 +374,24 @@ object Authenticator {

// Step 7

// txAuthSimple leads to a prompt that the user has to confirm. This prompt has to be shown
// before the usual user presence check as per spec, but we omit it if there are no
// applicable credentials.
if (activeExtensions.containsKey(Extension.TxAuthSimple) && numberOfCredentials > 0) {
val txAuthSimpleInput =
activeExtensions[Extension.TxAuthSimple] as TxAuthSimpleAuthenticateInput
// The actual prompt confirmed by the user differs from the requested prompt only by
// potentially containing additional newlines. We simply replace the extension input
// with the prompt that was actually shown to keep extension handling simple.
val actualPrompt = context.confirmTransactionWithUser(rpId, txAuthSimpleInput.prompt)
?: CTAP_ERR(OperationDenied)
activeExtensions[Extension.TxAuthSimple] = TxAuthSimpleAuthenticateInput(actualPrompt)
// Introduce a small delay between the transaction confirmation and the usual
// GetAssertion confirmation, otherwise the user may inadvertently confirm both with one
// tap.
delay(500)
}

// Since requests requiring user verification may ask the user to confirm their device
// credentials, we upgrade them to also require user presence.
if (requireUserVerification)
Expand All @@ -383,7 +414,7 @@ object Authenticator {
// We have not found any credentials, ask the user for permission to reveal this fact.
Ctap2RequestInfo(AUTHENTICATE_NO_CREDENTIALS, rpId)
}
if (requireUserPresence && !context.confirmWithUser(requestInfo))
if (requireUserPresence && !context.confirmRequestWithUser(requestInfo))
CTAP_ERR(OperationDenied)
if (!requireUserPresence)
Log.i(TAG, "Processing silent GetAssertion request")
Expand Down Expand Up @@ -492,7 +523,11 @@ object Authenticator {
CborTextString("U2F_V2")
)
),
GET_INFO_RESPONSE_EXTENSIONS to Extension.identifiersAsCbor,
GET_INFO_RESPONSE_EXTENSIONS to
if (context.isHidTransport)
Extension.identifiersAsCbor
else
Extension.noDisplayIdentifiersAsCbor,
GET_INFO_RESPONSE_AAGUID to CborByteString(WEARAUTHN_AAGUID),
GET_INFO_RESPONSE_OPTIONS to CborTextStringMap(optionsMap),
GET_INFO_RESPONSE_MAX_MSG_SIZE to CborLong(MAX_CBOR_MSG_SIZE),
Expand Down Expand Up @@ -568,7 +603,8 @@ object Authenticator {

private fun parseExtensionInputs(
extensions: Map<String, CborValue>?,
action: AuthenticatorAction
action: AuthenticatorAction,
canUseDisplay: Boolean
): Map<Extension, ExtensionInput> {
if (extensions == null)
return mapOf()
Expand All @@ -577,7 +613,7 @@ object Authenticator {
}.map {
val extension = Extension.fromIdentifier(it.key)
// parseInput throws an appropriate exception if the input is not of the correct form.
Pair(extension, extension.parseInput(it.value, action))
Pair(extension, extension.parseInput(it.value, action, canUseDisplay))
}.toMap()
}

Expand Down Expand Up @@ -652,6 +688,13 @@ object Authenticator {
require(input is NoInput)
Extension.identifiersAsCbor
}
Extension.TxAuthSimple -> {
require(action == AUTHENTICATE)
require(input is TxAuthSimpleAuthenticateInput)
// At this point, either we have returned an OperationDenied error or the user has
// confirmed the prompt (with added line breaks).
CborTextString(input.prompt)
}
Extension.UserVerificationMethod -> {
require(input is NoInput)
val keyProtectionType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,20 @@ data class HmacSecretAuthenticateInput(
val saltAuth: ByteArray
) : ExtensionInput

data class TxAuthSimpleAuthenticateInput(val prompt: String) : ExtensionInput

enum class Extension(val identifier: String) {
HmacSecret("hmac-secret"),
SupportedExtensions("exts"),
TxAuthSimple("txAuthSimple"),
UserVerificationMethod("uvm");

@ExperimentalUnsignedTypes
fun parseInput(input: CborValue, action: AuthenticatorAction): ExtensionInput {
fun parseInput(
input: CborValue,
action: AuthenticatorAction,
canUseDisplay: Boolean
): ExtensionInput {
require(action == AUTHENTICATE || action == REGISTER)
return when (this) {
HmacSecret -> {
Expand Down Expand Up @@ -232,6 +239,18 @@ enum class Extension(val identifier: String) {
CTAP_ERR(InvalidParameter, "Input was not 'true' for exts")
NoInput
}
TxAuthSimple -> {
if (action != AUTHENTICATE)
CTAP_ERR(InvalidParameter, "txAuthSimple not supported during MakeCredential")
// txAuthSimple requires interactive confirmation and therefore requires a transport
// that can use the display.
if (!canUseDisplay)
CTAP_ERR(UnsupportedExtension)
val prompt = input.unbox<String>()
if (prompt.isBlank())
CTAP_ERR(InvalidParameter, "Input was only whitespace for txAuthSimple")
TxAuthSimpleAuthenticateInput(prompt)
}
UserVerificationMethod -> {
if (!input.unbox<Boolean>())
CTAP_ERR(InvalidParameter, "Input was not 'true' for uvm")
Expand All @@ -242,9 +261,13 @@ enum class Extension(val identifier: String) {

companion object {
val identifiers = values().map { it.identifier }
val noDisplayIdentifiers = values().filter { it != TxAuthSimple }.map { it.identifier }

@ExperimentalUnsignedTypes
val identifiersAsCbor = CborArray(identifiers.map { CborTextString(it) }.toTypedArray())
@ExperimentalUnsignedTypes
val noDisplayIdentifiersAsCbor =
CborArray(noDisplayIdentifiers.map { CborTextString(it) }.toTypedArray())

fun fromIdentifier(identifier: String) =
requireNotNull(values().associateBy(Extension::identifier)[identifier])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ class TransactionManager(private val authenticatorContext: AuthenticatorContext)
// CONDITIONS_NOT_SATISFIED while waiting.
activeU2fConfirmation = U2fContinuation(
message,
async { authenticatorContext.confirmWithUser(requestInfo) },
async { authenticatorContext.confirmRequestWithUser(requestInfo) },
cont
)
rearmU2fRetryTimeout()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ import me.henneke.wearauthn.fido.ApduException
import me.henneke.wearauthn.fido.CommandApdu
import me.henneke.wearauthn.fido.ResponseApdu
import me.henneke.wearauthn.fido.StatusWord
import me.henneke.wearauthn.fido.context.AuthenticatorContext
import me.henneke.wearauthn.fido.context.AuthenticatorSpecialStatus
import me.henneke.wearauthn.fido.context.*
import me.henneke.wearauthn.fido.context.AuthenticatorSpecialStatus.RESET
import me.henneke.wearauthn.fido.context.AuthenticatorSpecialStatus.USER_NOT_AUTHENTICATED
import me.henneke.wearauthn.fido.context.RequestInfo
import me.henneke.wearauthn.fido.ctap2.Authenticator
import me.henneke.wearauthn.ui.isDoNotDisturbEnabled
import me.henneke.wearauthn.ui.showToast
Expand Down Expand Up @@ -218,10 +216,14 @@ class NfcAuthenticatorService : HostApduService(), CoroutineScope {
}
}

override suspend fun confirmWithUser(info: RequestInfo): Boolean {
override suspend fun confirmRequestWithUser(info: RequestInfo): Boolean {
// User presence is always certain with NFC transport.
return true
}

override suspend fun confirmTransactionWithUser(rpId: String, prompt: String): String? {
throw IllegalStateException("Transaction confirmation not possible via NFC")
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import android.content.DialogInterface
import android.os.PowerManager
import android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP
import android.os.PowerManager.FULL_WAKE_LOCK
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import kotlinx.android.synthetic.main.timed_accept_deny_dialog.*
import me.henneke.wearauthn.R

Expand All @@ -35,6 +37,9 @@ private const val DEFAULT_TIMEOUT = 5_000L

class TimedAcceptDenyDialog(context: Context) : Dialog(context) {

var messageLineBreaks: List<Int>? = null
private set

private var wakeOnShow = false
private var vibrateOnShow = false
private var positiveButtonListener: DialogInterface.OnClickListener? = null
Expand Down Expand Up @@ -66,6 +71,19 @@ class TimedAcceptDenyDialog(context: Context) : Dialog(context) {
negativeButton.setOnClickListener(actionHandler)
negativeTimeout.setOnTimerFinishedListener(actionHandler)
positiveButton.setOnClickListener(actionHandler)
// The txAuthSimple extension requires us to record the line breaks inserted into the
// message by the text rendering engine. This information can only be extracted reliably
// right before a draw. Since we do not expect the message to change after the dialog is
// shown, we remove the listener after the text layout has been obtained once.
messageView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (messageView.layout != null) {
computeMessageLineBreaks()
messageView.viewTreeObserver.removeOnPreDrawListener(this)
}
return true
}
})
}

override fun onStart() {
Expand Down Expand Up @@ -158,4 +176,10 @@ class TimedAcceptDenyDialog(context: Context) : Dialog(context) {
fun setWakeOnShow(wakeOnShow: Boolean) {
this.wakeOnShow = wakeOnShow
}

private fun computeMessageLineBreaks() {
val layout = messageView.layout
if (layout != null)
messageLineBreaks = (0 until layout.lineCount - 1).map { layout.getLineEnd(it) }
}
}
Loading

0 comments on commit 9ba054c

Please sign in to comment.