diff --git a/app/src/main/java/pl/lebihan/authnkey/AuthnkeyError.kt b/app/src/main/java/pl/lebihan/authnkey/AuthnkeyError.kt new file mode 100644 index 0000000..108811e --- /dev/null +++ b/app/src/main/java/pl/lebihan/authnkey/AuthnkeyError.kt @@ -0,0 +1,17 @@ +package pl.lebihan.authnkey + +sealed class AuthnkeyError(message: String) : Exception(message) { + // Connection errors + class NotIsoDepTag : AuthnkeyError("Not an ISO-DEP tag") + class FidoAppletNotFound : AuthnkeyError("FIDO applet not found") + class ConnectionFailed : AuthnkeyError("Connection failed") + class NotConnected : AuthnkeyError("Not connected") + + // PIN protocol errors + class PinProtocolNotInitialized : AuthnkeyError("PIN protocol not initialized") + class PinProtocolInitFailed : AuthnkeyError("PIN protocol initialization failed") + + // Authentication errors + class UserVerificationRequiredNoPin : AuthnkeyError("User verification required but no PIN set") + class PinBlocked : AuthnkeyError("PIN is blocked") +} diff --git a/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt b/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt index 73861ed..f7fb8f8 100644 --- a/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt +++ b/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt @@ -10,6 +10,7 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.nfc.NfcAdapter import android.nfc.Tag +import android.nfc.TagLostException import android.nfc.tech.IsoDep import android.os.Build import android.os.Bundle @@ -369,11 +370,11 @@ class CredentialProviderActivity : AppCompatActivity() { bottomSheet?.getCurrentPinIfValid()?.let { pendingPin = it } bottomSheet?.showPinInput(false) - val isoDep = IsoDep.get(tag) ?: throw Exception("Not an ISO-DEP tag") + val isoDep = IsoDep.get(tag) ?: throw AuthnkeyError.NotIsoDepTag() val transport = NfcTransport(isoDep) if (!transport.selectFidoApplet()) { - throw Exception("Failed to select FIDO applet") + throw AuthnkeyError.FidoAppletNotFound() } currentTransport = transport @@ -387,7 +388,7 @@ class CredentialProviderActivity : AppCompatActivity() { } catch (e: Exception) { Log.e(TAG, "NFC error", e) - setInstruction(getString(R.string.error_retry_format, e.message ?: "Unknown error")) + setInstruction(getString(R.string.error_retry_format, e.toUserMessage(this@CredentialProviderActivity))) setState(CredentialBottomSheet.State.ERROR) showProgress(false) } @@ -408,7 +409,7 @@ class CredentialProviderActivity : AppCompatActivity() { val transport = withContext(Dispatchers.IO) { UsbTransport.create(usbManager, device) - } ?: throw Exception("Failed to initialize USB connection") + } ?: throw AuthnkeyError.ConnectionFailed() currentTransport = transport pinProtocol = PinProtocol(transport) @@ -420,7 +421,7 @@ class CredentialProviderActivity : AppCompatActivity() { } catch (e: Exception) { Log.e(TAG, "USB error", e) - setInstruction(getString(R.string.error_retry_format, e.message ?: "Unknown error")) + setInstruction(getString(R.string.error_retry_format, e.toUserMessage(this@CredentialProviderActivity))) setState(CredentialBottomSheet.State.ERROR) showProgress(false) } @@ -431,8 +432,8 @@ class CredentialProviderActivity : AppCompatActivity() { scope.launch { try { val json = JSONObject(requestJson!!) - val transport = currentTransport ?: throw Exception("No transport") - val protocol = pinProtocol ?: throw Exception("No PIN protocol") + val transport = currentTransport ?: throw AuthnkeyError.NotConnected() + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() // Get device info to check PIN requirements and CTAP version val infoResponse = withContext(Dispatchers.IO) { @@ -452,7 +453,7 @@ class CredentialProviderActivity : AppCompatActivity() { } // UV required but device has no PIN - fail userVerification == UserVerification.REQUIRED && !deviceHasPin -> { - throw Exception(getString(R.string.error_uv_required_no_pin)) + throw AuthnkeyError.UserVerificationRequiredNoPin() } // UV required/preferred and device has PIN - need to get PIN userVerification != UserVerification.DISCOURAGED && deviceHasPin -> { @@ -486,7 +487,7 @@ class CredentialProviderActivity : AppCompatActivity() { if (e.error == CTAP.Error.PIN_REQUIRED || e.error == CTAP.Error.PIN_AUTH_INVALID) { Log.d(TAG, "Authenticator requires PIN despite UV=discouraged") - val protocol = pinProtocol ?: throw Exception("No PIN protocol") + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() val retries = withContext(Dispatchers.IO) { protocol.getPinRetries() }.getOrDefault(8) showPinDialog(retries, json) } else { @@ -509,12 +510,12 @@ class CredentialProviderActivity : AppCompatActivity() { private fun authenticateAndExecute(pin: String, requestJson: JSONObject) { scope.launch { try { - val protocol = pinProtocol ?: throw Exception("PIN protocol not initialized") + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() setInstruction(getString(R.string.instruction_initializing)) val initialized = withContext(Dispatchers.IO) { protocol.initialize() } if (!initialized) { - throw Exception("Failed to initialize PIN protocol") + throw AuthnkeyError.PinProtocolInitFailed() } // Determine permissions and rpId based on operation type @@ -539,12 +540,12 @@ class CredentialProviderActivity : AppCompatActivity() { if (retries > 0) { runOnUiThread { showProgress(false) - setInstruction(getString(R.string.pin_invalid_retries, retries)) + setInstruction(getString(R.string.pin_incorrect_retries, retries)) setState(CredentialBottomSheet.State.PIN) bottomSheet?.showPinInput(true) } } else { - throw Exception(getString(R.string.error_pin_blocked)) + throw AuthnkeyError.PinBlocked() } } else { throw e @@ -563,7 +564,7 @@ class CredentialProviderActivity : AppCompatActivity() { private suspend fun executeRequest(requestJson: JSONObject, pinProtocol: PinProtocol?) { try { - val transport = currentTransport ?: throw Exception("No transport") + val transport = currentTransport ?: throw AuthnkeyError.NotConnected() if (isCreateRequest) { executeCreateCredential(transport, requestJson, pinProtocol) @@ -1074,7 +1075,7 @@ class CredentialProviderActivity : AppCompatActivity() { private fun handleError(e: Exception) { runOnUiThread { showProgress(false) - setInstruction(getString(R.string.error_format, e.message ?: "Unknown error")) + setInstruction(getString(R.string.error_format, e.toUserMessage(this))) setState(CredentialBottomSheet.State.ERROR) } } diff --git a/app/src/main/java/pl/lebihan/authnkey/ErrorMapping.kt b/app/src/main/java/pl/lebihan/authnkey/ErrorMapping.kt new file mode 100644 index 0000000..2249a52 --- /dev/null +++ b/app/src/main/java/pl/lebihan/authnkey/ErrorMapping.kt @@ -0,0 +1,42 @@ +package pl.lebihan.authnkey + +import android.content.Context +import android.nfc.TagLostException + +fun Throwable.toUserMessage(context: Context): String = when (this) { + // Android NFC exception + is TagLostException -> context.getString(R.string.error_tag_lost) + + // CTAP protocol errors + is CTAP.Exception -> this.error.toUserMessage(context) + + // App-specific errors + is AuthnkeyError.NotIsoDepTag -> context.getString(R.string.error_not_supported_tag) + is AuthnkeyError.FidoAppletNotFound -> context.getString(R.string.error_not_security_key) + is AuthnkeyError.ConnectionFailed -> context.getString(R.string.error_connection_failed) + is AuthnkeyError.NotConnected -> context.getString(R.string.error_not_connected) + is AuthnkeyError.PinProtocolNotInitialized -> context.getString(R.string.error_key_disconnected) + is AuthnkeyError.PinProtocolInitFailed -> context.getString(R.string.error_communication_failed) + is AuthnkeyError.UserVerificationRequiredNoPin -> context.getString(R.string.error_uv_required_no_pin) + is AuthnkeyError.PinBlocked -> context.getString(R.string.error_pin_blocked) + + // Fallback + else -> this.message ?: context.getString(R.string.error_unknown) +} + +private fun CTAP.Error.toUserMessage(context: Context): String = when (this) { + CTAP.Error.NO_CREDENTIALS -> context.getString(R.string.error_no_credentials) + CTAP.Error.PIN_INVALID -> context.getString(R.string.error_pin_incorrect) + CTAP.Error.PIN_BLOCKED -> context.getString(R.string.error_pin_blocked) + CTAP.Error.PIN_NOT_SET -> context.getString(R.string.error_pin_not_set) + CTAP.Error.PIN_AUTH_INVALID -> context.getString(R.string.error_pin_auth_invalid) + CTAP.Error.PIN_AUTH_BLOCKED -> context.getString(R.string.error_pin_auth_blocked) + CTAP.Error.CREDENTIAL_EXCLUDED -> context.getString(R.string.error_credential_excluded) + CTAP.Error.OPERATION_DENIED -> context.getString(R.string.error_operation_denied) + CTAP.Error.USER_ACTION_TIMEOUT -> context.getString(R.string.error_user_action_timeout) + CTAP.Error.TIMEOUT -> context.getString(R.string.error_timeout) + CTAP.Error.KEY_STORE_FULL -> context.getString(R.string.error_key_store_full) + CTAP.Error.UNSUPPORTED_ALGORITHM -> context.getString(R.string.error_unsupported_algorithm) + CTAP.Error.KEEPALIVE_CANCEL -> context.getString(R.string.error_operation_cancelled) + else -> context.getString(R.string.error_ctap_unknown, this.name) +} diff --git a/app/src/main/java/pl/lebihan/authnkey/MainActivity.kt b/app/src/main/java/pl/lebihan/authnkey/MainActivity.kt index f39d948..05e0b40 100644 --- a/app/src/main/java/pl/lebihan/authnkey/MainActivity.kt +++ b/app/src/main/java/pl/lebihan/authnkey/MainActivity.kt @@ -11,6 +11,7 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.nfc.NfcAdapter import android.nfc.Tag +import android.nfc.TagLostException import android.nfc.tech.IsoDep import android.os.Build import android.os.Bundle @@ -185,11 +186,11 @@ class MainActivity : AppCompatActivity() { currentTransport?.close() currentTransport = null - val isoDep = IsoDep.get(tag) ?: throw Exception("Not an ISO-DEP tag") + val isoDep = IsoDep.get(tag) ?: throw AuthnkeyError.NotIsoDepTag() val transport = NfcTransport(isoDep) if (!transport.selectFidoApplet()) { - throw Exception("Failed to select FIDO applet") + throw AuthnkeyError.FidoAppletNotFound() } currentTransport = transport @@ -216,7 +217,7 @@ class MainActivity : AppCompatActivity() { } } catch (e: Exception) { - statusText.text = getString(R.string.nfc_error, e.message ?: "") + statusText.text = getString(R.string.nfc_error, e.toUserMessage(this@MainActivity)) updateConnectionStatus() } } @@ -299,7 +300,7 @@ class MainActivity : AppCompatActivity() { val transport = withContext(Dispatchers.IO) { UsbTransport.create(usbManager, device) - } ?: throw Exception("Failed to initialize FIDO communication") + } ?: throw AuthnkeyError.ConnectionFailed() currentTransport = transport pinProtocol = PinProtocol(transport) @@ -309,7 +310,7 @@ class MainActivity : AppCompatActivity() { statusText.text = getString(R.string.usb_connected) } catch (e: Exception) { - statusText.text = getString(R.string.usb_error, e.message ?: "") + statusText.text = getString(R.string.usb_error, e.toUserMessage(this@MainActivity)) updateConnectionStatus() } } @@ -362,7 +363,7 @@ class MainActivity : AppCompatActivity() { scope.launch { try { - val transport = currentTransport ?: throw Exception("No key connected") + val transport = currentTransport ?: throw AuthnkeyError.NotConnected() resultText.text = getString(R.string.reading_device_info) @@ -389,7 +390,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_generic, e.message ?: "") + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null handleDisconnect() } @@ -402,7 +403,7 @@ class MainActivity : AppCompatActivity() { scope.launch { try { - val transport = currentTransport ?: throw Exception("No key connected") + val transport = currentTransport ?: throw AuthnkeyError.NotConnected() resultText.text = getString(R.string.checking_cred_mgmt) @@ -426,7 +427,7 @@ class MainActivity : AppCompatActivity() { return@launch } - val protocol = pinProtocol ?: throw Exception("PIN protocol not initialized") + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() val retries = withContext(Dispatchers.IO) { protocol.getPinRetries() }.getOrElse { e -> if (e is java.io.IOException) throw e resultText.text = getString(R.string.error_could_not_get_pin_status) @@ -448,7 +449,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_generic, e.message ?: "") + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null handleDisconnect() } @@ -486,8 +487,8 @@ class MainActivity : AppCompatActivity() { scope.launch { try { - val transport = currentTransport ?: throw Exception("No key connected") - val protocol = pinProtocol ?: throw Exception("PIN protocol not initialized") + val transport = currentTransport ?: throw AuthnkeyError.NotConnected() + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() resultText.text = getString(R.string.authenticating) @@ -507,18 +508,8 @@ class MainActivity : AppCompatActivity() { if (usePreviewCommand) protocol.requestPinToken(pin) else protocol.requestPinToken(pin, PinProtocol.PERMISSION_CM) }.onFailure { e -> - when { - e is java.io.IOException -> throw e - e is CTAP.Exception && e.error == CTAP.Error.PIN_INVALID -> { - resultText.text = getString(R.string.error_invalid_pin) - } - e is CTAP.Exception && e.error == CTAP.Error.PIN_BLOCKED -> { - resultText.text = getString(R.string.error_pin_blocked) - } - else -> { - resultText.text = getString(R.string.error_generic, e.message ?: "") - } - } + if (e is java.io.IOException) throw e + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null return@launch } @@ -533,7 +524,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_metadata, it.message ?: "") + resultText.text = getString(R.string.error_metadata, it.toUserMessage(this@MainActivity)) pendingAction = null } return@launch @@ -555,7 +546,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = outputFormatter.formatEnumerateRpsError(metadata, it.message ?: "") + resultText.text = outputFormatter.formatEnumerateRpsError(metadata, it.toUserMessage(this@MainActivity)) pendingAction = null } return@launch @@ -586,7 +577,7 @@ class MainActivity : AppCompatActivity() { OutputFormatter.RelyingPartyWithCredentials( relyingParty = rp, credentials = null, - error = credsResult.exceptionOrNull()?.message + error = credsResult.exceptionOrNull()?.toUserMessage(this@MainActivity) ) ) } else { @@ -614,7 +605,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_generic_with_trace, e.message ?: "", e.stackTraceToString()) + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null handleDisconnect() } @@ -627,7 +618,7 @@ class MainActivity : AppCompatActivity() { scope.launch { try { - val protocol = pinProtocol ?: throw Exception("PIN protocol not initialized") + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() resultText.text = getString(R.string.checking_pin_status) @@ -639,9 +630,9 @@ class MainActivity : AppCompatActivity() { val confirmPinEdit = dialogView.findViewById(R.id.confirmPin) val message = if (retries != null) { - "PIN retries remaining: $retries" + getString(R.string.pin_retries_status, retries) } else { - "Could not get PIN status" + getString(R.string.error_pin_status) } pendingAction = null @@ -676,7 +667,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_generic, e.message ?: "") + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null handleDisconnect() } @@ -689,7 +680,7 @@ class MainActivity : AppCompatActivity() { scope.launch { try { - val protocol = pinProtocol ?: throw Exception("PIN protocol not initialized") + val protocol = pinProtocol ?: throw AuthnkeyError.PinProtocolNotInitialized() resultText.text = getString(R.string.initializing_pin_protocol) @@ -730,7 +721,7 @@ class MainActivity : AppCompatActivity() { if (isNfcDisconnected()) { showNfcReconnectDialog() } else { - resultText.text = getString(R.string.error_generic, e.message ?: "") + resultText.text = e.toUserMessage(this@MainActivity) pendingAction = null } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 265e63c..89ac5b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,19 +35,43 @@ Continue PIN must be at least 4 characters Enter PIN (%1$d retries remaining) - Invalid PIN. %1$d retries remaining + Incorrect PIN. %1$d retries remaining - Error: %1$s\n\nTry again or cancel - Error: %1$s\n\nTry again + %1$s\n\nTry again or cancel + %1$s\n\nTry again User cancelled No valid request found Error: %1$s Error: %1$s\n\n%2$s Error getting metadata: %1$s - Error: Could not get PIN status - Error: New PINs don\'t match - User verification required but no PIN is set on your security key + Could not get PIN status + New PINs don\'t match + Verification required but no PIN is set on your security key + + + Lost contact with the security key + This NFC tag is not a security key + Security key not recognized + Could not connect to the security key + No security key connected + The security key disconnected + Could not communicate with the security key + No passkeys found on this security key + Incorrect PIN + PIN is blocked + No PIN configured on this security key + PIN authentication failed + Too many failed PIN attempts + This passkey already exists + Operation denied by security key + Operation cancelled + Touch was not detected in time + Operation timed out + Security key storage is full + Unsupported security algorithm + Security key error: %1$s + An unexpected error occurred Security Key @@ -75,16 +99,14 @@ Operation cancelled Reading device info… Checking credential management support… - Error: Could not parse device info - Error: PIN is blocked. Device needs to be reset. + Could not parse device info Could not get PIN status PIN Required Enter your PIN to list credentials.\n\nRetries remaining: %1$d - Error: PIN must be at least 4 characters + PIN must be at least 4 characters Authenticating… - Error: Failed to initialize PIN protocol + Could not establish secure connection Verifying PIN… - Error: Invalid PIN Getting credential metadata… Enumerating relying parties… Loading credentials for %1$s…