[Android] support devices with NFC restrictions

This commit is contained in:
Adam Velebil 2024-08-22 15:33:25 +02:00
parent bba92b8b54
commit 41349617df
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
11 changed files with 147 additions and 62 deletions

View File

@ -47,7 +47,7 @@ import com.yubico.authenticator.management.ManagementHandler
import com.yubico.authenticator.oath.AppLinkMethodChannel
import com.yubico.authenticator.oath.OathManager
import com.yubico.authenticator.oath.OathViewModel
import com.yubico.authenticator.yubikit.getDeviceInfo
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.YubiKitManager
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
@ -57,7 +57,6 @@ import com.yubico.yubikit.android.transport.usb.UsbConfiguration
import com.yubico.yubikit.core.Transport
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.smartcard.scp.KeyRef
import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
import com.yubico.yubikit.core.smartcard.scp.ScpKid
@ -124,7 +123,7 @@ class MainActivity : FlutterFragmentActivity() {
}
hasNfc = true
} catch (e: NfcNotAvailable) {
} catch (_: NfcNotAvailable) {
hasNfc = false
}
@ -320,7 +319,7 @@ class MainActivity : FlutterFragmentActivity() {
switchContext(preferredContext)
}
if (contextManager == null) {
if (contextManager == null && supportedContexts.isNotEmpty()) {
switchContext(DeviceManager.getPreferredContext(supportedContexts))
}

View File

@ -1,3 +1,19 @@
/*
* Copyright (C) 2023-2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yubico.authenticator.device
import com.yubico.yubikit.core.Transport
@ -17,7 +33,7 @@ val UnknownDevice = Info(
isLocked = false,
isSky = false,
isFips = false,
name = "Unrecognized device",
name = "unknown-device",
isNfc = false,
usbPid = null,
pinComplexity = false,
@ -50,4 +66,15 @@ fun unknownFido2DeviceInfo(transport: Transport) : Info {
return unknownDeviceWithCapability(transport, Capability.FIDO2.bit).copy(
name = "FIDO2 device"
)
}
fun restrictedNfcDeviceInfo(transport: Transport) : Info {
if (transport != Transport.NFC) {
return UnknownDevice
}
return UnknownDevice.copy(
isNfc = true,
name = "restricted-nfc"
)
}

View File

@ -16,8 +16,9 @@
package com.yubico.authenticator.yubikit
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.compatUtil
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.device.restrictedNfcDeviceInfo
import com.yubico.authenticator.device.unknownDeviceWithCapability
import com.yubico.authenticator.device.unknownFido2DeviceInfo
import com.yubico.authenticator.device.unknownOathDeviceInfo
@ -27,58 +28,98 @@ import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
import com.yubico.yubikit.core.fido.FidoConnection
import com.yubico.yubikit.core.otp.OtpConnection
import com.yubico.yubikit.core.smartcard.Apdu
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
import com.yubico.yubikit.fido.ctap.Ctap2Session
import com.yubico.yubikit.management.DeviceInfo
import com.yubico.yubikit.oath.OathSession
import com.yubico.yubikit.support.DeviceUtil
import org.slf4j.LoggerFactory
suspend fun getDeviceInfo(device: YubiKeyDevice): Info? {
val pid = (device as? UsbYubiKeyDevice)?.pid
val logger = LoggerFactory.getLogger("getDeviceInfo")
class DeviceInfoHelper {
companion object {
private val logger = LoggerFactory.getLogger("DeviceInfoHelper")
private val nfcTagReaderAid = byteArrayOf(0xD2.toByte(), 0x76, 0, 0, 0x85.toByte(), 1, 1)
private val uri = "yubico.com/getting-started".toByteArray()
private val restrictedNfcBytes =
byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri
val deviceInfo = runCatching {
device.withConnection<SmartCardConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
logger.debug("Smart card connection not available: {}", t.message)
device.withConnection<OtpConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
logger.debug("OTP connection not available: {}", t.message)
device.withConnection<FidoConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
logger.debug("FIDO connection not available: {}", t.message)
return SkyHelper(compatUtil).getDeviceInfo(device)
}.getOrElse {
// this is not a YubiKey
logger.debug("Probing unknown device")
try {
device.openConnection(SmartCardConnection::class.java).use { smartCardConnection ->
suspend fun getDeviceInfo(device: YubiKeyDevice): Info? {
val pid = (device as? UsbYubiKeyDevice)?.pid
val deviceInfo = runCatching {
device.withConnection<SmartCardConnection, DeviceInfo> {
DeviceUtil.readInfo(
it,
pid
)
}
}.recoverCatching { t ->
logger.debug("Smart card connection not available: {}", t.message)
device.withConnection<OtpConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
logger.debug("OTP connection not available: {}", t.message)
device.withConnection<FidoConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
logger.debug("FIDO connection not available: {}", t.message)
return SkyHelper(compatUtil).getDeviceInfo(device)
}.getOrElse {
// this is not a YubiKey
logger.debug("Probing unknown device")
try {
// if OATH session is available use it
OathSession(smartCardConnection)
logger.debug("Device supports OATH")
return unknownOathDeviceInfo(device.transport)
} catch (applicationNotAvailable: ApplicationNotAvailableException) {
try {
// probe for CTAP2 availability
Ctap2Session(smartCardConnection)
logger.debug("Device supports FIDO2")
return unknownFido2DeviceInfo(device.transport)
} catch (applicationNotAvailable: ApplicationNotAvailableException) {
logger.debug("Device not recognized")
return unknownDeviceWithCapability(device.transport)
}
device.openConnection(SmartCardConnection::class.java)
.use { smartCardConnection ->
try {
// if OATH session is available use it
OathSession(smartCardConnection)
logger.debug("Device supports OATH")
return unknownOathDeviceInfo(device.transport)
} catch (_: ApplicationNotAvailableException) {
try {
// probe for CTAP2 availability
Ctap2Session(smartCardConnection)
logger.debug("Device supports FIDO2")
return unknownFido2DeviceInfo(device.transport)
} catch (_: ApplicationNotAvailableException) {
// probe for NFC restricted device
if (isNfcRestricted(smartCardConnection)) {
logger.debug("Device has restricted NFC")
return restrictedNfcDeviceInfo(device.transport)
}
logger.debug("Device not recognized")
return unknownDeviceWithCapability(device.transport)
}
}
}
} catch (e: Exception) {
// no smart card connectivity
logger.error("Failure getting device info", e)
return null
}
}
val name = DeviceUtil.getName(deviceInfo, pid?.type)
return Info(name, device is NfcYubiKeyDevice, pid?.value, deviceInfo)
}
private fun isNfcRestricted(connection: SmartCardConnection): Boolean =
restrictedNfcBytes.contentEquals(readNdef(connection).also {
logger.debug("ndef: {}", it)
})
private fun readNdef(connection: SmartCardConnection): ByteArray? = try {
with(SmartCardProtocol(connection)) {
select(nfcTagReaderAid)
sendAndReceive(Apdu(0x00, 0xA4, 0x00, 0x0C, byteArrayOf(0xE1.toByte(), 0x04)))
sendAndReceive(Apdu(0x00, 0xB0, 0x00, 0x00, null))
}
} catch (e: Exception) {
// no smart card connectivity
logger.error("Failure getting device info", e)
return null
logger.debug("Failed to read ndef tag: ", e)
null
}
}
val name = DeviceUtil.getName(deviceInfo, pid?.type)
return Info(name, device is NfcYubiKeyDevice, pid?.value, deviceInfo)
}

View File

@ -157,8 +157,18 @@ class AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier {
.maybeWhen(data: (data) => [data.node], orElse: () => []);
}
final androidDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) => ref.watch(androidYubikeyProvider));
final androidDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>((ref) {
return ref.watch(androidYubikeyProvider).when(data: (d) {
if (d.name == 'restricted-nfc' || d.name == 'unknown-device') {
return AsyncError(d.name, StackTrace.current);
}
return AsyncData(d);
}, error: (Object error, StackTrace stackTrace) {
return AsyncError(error, stackTrace);
}, loading: () {
return const AsyncLoading();
});
});
class AndroidCurrentDeviceNotifier extends CurrentDeviceNotifier {
@override

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -78,6 +78,16 @@ class DeviceErrorScreen extends ConsumerWidget {
),
header: l10n.s_unknown_device,
),
'restricted-nfc' => HomeMessagePage(
centered: true,
graphic: Icon(
Symbols.warning,
size: 96,
color: Theme.of(context).colorScheme.error,
),
header: l10n.s_restricted_nfc,
message: l10n.l_restricted_nfc,
),
_ => HomeMessagePage(
centered: true,
graphic: Image.asset(

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2023 Yubico.
* Copyright (C) 2022-2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -119,19 +119,7 @@ class MainPage extends ConsumerWidget {
data: (data) {
final section = ref.watch(currentSectionProvider);
final capabilities = section.capabilities;
if (data.info.supportedCapabilities.isEmpty &&
data.name == 'Unrecognized device') {
return HomeMessagePage(
centered: true,
graphic: Icon(
Symbols.help,
size: 96,
color: Theme.of(context).colorScheme.error,
),
header: l10n.s_yk_not_recognized,
);
} else if (section.getAvailability(data) ==
Availability.unsupported) {
if (section.getAvailability(data) == Availability.unsupported) {
return MessagePage(
title: section.getDisplayName(l10n),
capabilities: capabilities,

View File

@ -168,6 +168,7 @@
"l_insert_or_tap_yk": "YubiKey anschließen oder dagegenhalten",
"l_unplug_yk": "Entfernen Sie Ihren YubiKey",
"l_reinsert_yk": "Schließen Sie Ihren YubiKey wieder an",
"l_restricted_nfc": null,
"l_place_on_nfc_reader": "Halten Sie Ihren YubiKey zum NFC-Leser",
"l_replace_yk_on_reader": "Halten Sie Ihren YubiKey wieder zum Leser",
"l_remove_yk_from_reader": "Entfernen Sie Ihren YubiKey vom NFC-Leser",
@ -229,6 +230,7 @@
"l_no_yk_present": "Kein YubiKey vorhanden",
"s_unknown_type": "Unbekannter Typ",
"s_unknown_device": "Unbekanntes Gerät",
"s_restricted_nfc": null,
"s_unsupported_yk": "Nicht unterstützter YubiKey",
"s_yk_not_recognized": "Geräte nicht erkannt",
"p_operation_failed_try_again": null,

View File

@ -168,6 +168,7 @@
"l_insert_or_tap_yk": "Insert or tap a YubiKey",
"l_unplug_yk": "Unplug your YubiKey",
"l_reinsert_yk": "Reinsert your YubiKey",
"l_restricted_nfc": "To remove NFC restrictions, connect the YubiKey to a USB port.",
"l_place_on_nfc_reader": "Place your YubiKey on the NFC reader",
"l_replace_yk_on_reader": "Place your YubiKey back on the reader",
"l_remove_yk_from_reader": "Remove your YubiKey from the NFC reader",
@ -229,6 +230,7 @@
"l_no_yk_present": "No YubiKey present",
"s_unknown_type": "Unknown type",
"s_unknown_device": "Unrecognized device",
"s_restricted_nfc": "NFC functionality restricted",
"s_unsupported_yk": "Unsupported YubiKey",
"s_yk_not_recognized": "Device not recognized",
"p_operation_failed_try_again": "The operation failed, please try again.",

View File

@ -168,6 +168,7 @@
"l_insert_or_tap_yk": "Insérez ou appuyez sur YubiKey",
"l_unplug_yk": "Retirez votre YubiKey",
"l_reinsert_yk": "Réinsérez votre YubiKey",
"l_restricted_nfc": null,
"l_place_on_nfc_reader": "Placez votre YubiKey sur le lecteur NFC",
"l_replace_yk_on_reader": "Replacez votre YubiKey sur le lecteur",
"l_remove_yk_from_reader": "Retirez votre YubiKey du lecteur NFC",
@ -229,6 +230,7 @@
"l_no_yk_present": "Aucune YubiKey présente",
"s_unknown_type": "Type inconnu",
"s_unknown_device": "Appareil non reconnu",
"s_restricted_nfc": null,
"s_unsupported_yk": "YubiKey non prise en charge",
"s_yk_not_recognized": "Appareil non reconnu",
"p_operation_failed_try_again": "L'opération a échoué, veuillez réessayer.",

View File

@ -168,6 +168,7 @@
"l_insert_or_tap_yk": "YubiKeyを挿入またはタップしてください",
"l_unplug_yk": "YubiKeyを抜いてください",
"l_reinsert_yk": "YubiKeyを再挿入してください",
"l_restricted_nfc": null,
"l_place_on_nfc_reader": "YubiKeyをNFCリーダーに置いてください",
"l_replace_yk_on_reader": "YubiKeyをNFCリーダーに戻してください",
"l_remove_yk_from_reader": "YubiKeyをNFCリーダーから外してください",
@ -229,6 +230,7 @@
"l_no_yk_present": "YubiKeyがありません",
"s_unknown_type": "不明なタイプ",
"s_unknown_device": "認識されないデバイス",
"s_restricted_nfc": null,
"s_unsupported_yk": "サポートされていないYubiKey",
"s_yk_not_recognized": "デバイスが認識されません",
"p_operation_failed_try_again": "操作に失敗しました。もう一度やり直してください。",

View File

@ -168,6 +168,7 @@
"l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey",
"l_unplug_yk": "Odłącz klucz YubiKey",
"l_reinsert_yk": "Ponownie podłącz YubiKey",
"l_restricted_nfc": null,
"l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC",
"l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku",
"l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC",
@ -229,6 +230,7 @@
"l_no_yk_present": "Nie wykryto YubiKey",
"s_unknown_type": "Nieznany typ",
"s_unknown_device": "Nierozpoznane urządzenie",
"s_restricted_nfc": null,
"s_unsupported_yk": "Nieobsługiwany klucz YubiKey",
"s_yk_not_recognized": "Urządzenie nie rozpoznane",
"p_operation_failed_try_again": null,