Browse Source

TPDU support, take 2 (#324)

We now know that some readers won't actually work with TPDU even if they advertise it. So, reintroduce the changes but now we only enable if the reader is a known one that requires TPDU or if the user enables it via a setting.

Co-authored-by: Vladimir Serbinenko <phcoder@gmail.com>
Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/324
Peter Cai 3 days ago
parent
commit
1c70ca7a70

+ 6 - 1
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt

@@ -363,7 +363,12 @@ open class DefaultEuiccChannelManager(
     override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
     override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
         withContext(Dispatchers.IO) {
         withContext(Dispatchers.IO) {
             usbManager.deviceList.values.forEach { device ->
             usbManager.deviceList.values.forEach { device ->
-                Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
+                Log.i(
+                    TAG,
+                    "Scanning USB device ${device.vendorId.toUInt().toString(16)}:${
+                        device.productId.toUInt().toString(16)
+                    }"
+                )
                 val iface = device.interfaces.smartCard ?: return@forEach
                 val iface = device.interfaces.smartCard ?: return@forEach
                 // If we don't have permission, tell UI code that we found a candidate device, but we
                 // If we don't have permission, tell UI code that we found a candidate device, but we
                 // need permission to be able to do anything with it
                 // need permission to be able to do anything with it

+ 73 - 0
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt

@@ -21,9 +21,82 @@ class UsbApduInterface(
 
 
     private var channels = mutableSetOf<Int>()
     private var channels = mutableSetOf<Int>()
 
 
+    // ATR parser
+    // Specs: ISO/IEC 7816-3:2006 8.2 Answer-to-Reset
+    // See also: https://en.wikipedia.org/wiki/Answer_to_reset
+    class ParsedAtr private constructor(
+        val ts: Byte?,
+        val t0: Byte?,
+        val ta1: Byte?,
+        val tb1: Byte?,
+        val tc1: Byte?,
+        val td1: Byte?,
+        val ta2: Byte?,
+        val tb2: Byte?,
+        val tc2: Byte?,
+        val td2: Byte?
+    ) {
+        companion object {
+            fun parse(atr: ByteArray): ParsedAtr {
+                val ts = atr[0]
+                val t0 = atr[1]
+                val tx1 = arrayOf<Byte?>(null, null, null, null)
+                val tx2 = arrayOf<Byte?>(null, null, null, null)
+                var pointer = 2
+
+                for (i in 0..3) {
+                    if (t0.toInt() and (0x10 shl i) != 0) {
+                        tx1[i] = atr[pointer]
+                        pointer++
+                    }
+                }
+
+                val td1 = tx1[3] ?: 0
+
+                for (i in 0..3) {
+                    if (td1.toInt() and (0x10 shl i) != 0) {
+                        tx2[i] = atr[pointer]
+                        pointer++
+                    }
+                }
+
+                return ParsedAtr(
+                    ts = ts, t0 = t0, ta1 = tx1[0], tb1 = tx1[1], tc1 = tx1[2], td1 = tx1[3],
+                    ta2 = tx2[0], tb2 = tx2[1], tc2 = tx2[2], td2 = tx2[3],
+                )
+            }
+        }
+    }
+
     override fun connect() {
     override fun connect() {
         ccidCtx.connect()
         ccidCtx.connect()
 
 
+        if (ccidCtx.useTpdu) {
+            // Send parameter selection
+            // Specs: USB-CCID 3.2.1 TPDU level of exchange
+            val parsedAtr = ParsedAtr.parse(atr!!)
+            val ta1 = parsedAtr.ta1 ?: 0x11.toByte()
+            val pts1 = ta1 // TODO: Check that reader supports baud rate proposed by the card
+            val pps = byteArrayOf(0xff.toByte(), 0x10.toByte(), pts1, 0x00.toByte())
+            Log.d(TAG, "PTS1=${pts1} PPS: ${pps.encodeHex()}")
+            ccidCtx.transceiver.sendXfrBlock(pps)
+
+            // Send Set Parameters
+            // Specs: USB-CCID 6.1.7 PC_to_RDR_SetParameters
+
+            val param = byteArrayOf(
+                pts1,
+                (if (parsedAtr.ts == 0x3F.toByte()) 0x02 else 0x00),
+                parsedAtr.tc1 ?: 0,
+                parsedAtr.tc2 ?: 0x0A,
+                0x00
+            )
+
+            Log.d(TAG, "Param: ${param.encodeHex()}")
+
+            ccidCtx.transceiver.sendParamBlock(param)
+        }
+
         // Send Terminal Capabilities
         // Send Terminal Capabilities
         // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
         // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
         val terminalCapabilities = buildCmd(
         val terminalCapabilities = buildCmd(

+ 10 - 2
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt

@@ -8,6 +8,8 @@ import android.hardware.usb.UsbInterface
 import android.hardware.usb.UsbManager
 import android.hardware.usb.UsbManager
 import im.angry.openeuicc.util.preferenceRepository
 import im.angry.openeuicc.util.preferenceRepository
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
 
 
 /**
 /**
  * A wrapper over an usb device + interface, manages the lifecycle independent
  * A wrapper over an usb device + interface, manages the lifecycle independent
@@ -20,7 +22,8 @@ class UsbCcidContext private constructor(
     private val conn: UsbDeviceConnection,
     private val conn: UsbDeviceConnection,
     private val bulkIn: UsbEndpoint,
     private val bulkIn: UsbEndpoint,
     private val bulkOut: UsbEndpoint,
     private val bulkOut: UsbEndpoint,
-    val verboseLoggingFlow: Flow<Boolean>
+    val verboseLoggingFlow: Flow<Boolean>,
+    val useTpdu: Boolean
 ) {
 ) {
     companion object {
     companion object {
         fun createFromUsbDevice(
         fun createFromUsbDevice(
@@ -33,11 +36,16 @@ class UsbCcidContext private constructor(
             val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice)
             val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice)
                 ?: return@runCatching null
                 ?: return@runCatching null
             if (!conn.claimInterface(usbInterface, true)) return@runCatching null
             if (!conn.claimInterface(usbInterface, true)) return@runCatching null
+
+            val forceTpduMode = runBlocking { context.preferenceRepository.forceTpduModeFlow.first() }
+            val useTpdu = forceTpduMode || isKnownTpduReader(usbDevice.vendorId, usbDevice.productId)
+
             UsbCcidContext(
             UsbCcidContext(
                 conn,
                 conn,
                 bulkIn,
                 bulkIn,
                 bulkOut,
                 bulkOut,
-                context.preferenceRepository.verboseLoggingFlow
+                context.preferenceRepository.verboseLoggingFlow,
+                useTpdu
             )
             )
         }.getOrNull()
         }.getOrNull()
     }
     }

+ 1 - 1
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt

@@ -95,4 +95,4 @@ data class UsbCcidDescription(
 
 
     val hasT0Protocol: Boolean
     val hasT0Protocol: Boolean
         get() = (dwProtocols and MASK_T0_PROTO) != 0
         get() = (dwProtocols and MASK_T0_PROTO) != 0
-}
+}

+ 72 - 0
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt

@@ -158,6 +158,46 @@ class UsbCcidTransceiver(
         }
         }
     }
     }
 
 
+    private fun receiveParamBlock(expectedSequenceNumber: Byte): ByteArray {
+        var response: ByteArray?
+        do {
+            response = receiveParamBlockImmediate(expectedSequenceNumber)
+        } while (response!![7] == 0x80.toByte())
+        return response
+    }
+
+    private fun receiveParamBlockImmediate(expectedSequenceNumber: Byte): ByteArray {
+        /*
+         * Some USB CCID devices (notably NitroKey 3) may time-out and need a subsequent poke to
+         * carry on communications.  No particular reason why the number 3 was chosen.  If we get a
+         * zero-sized reply (or a time-out), we try again.  Clamped retries prevent an infinite loop
+         * if things really turn sour.
+         */
+        var attempts = 3
+        Log.d(TAG, "Receive data block immediate seq=$expectedSequenceNumber")
+        var readBytes: Int
+        do {
+            readBytes = usbConnection.bulkTransfer(
+                usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
+            )
+            if (runBlocking { verboseLoggingFlow.first() }) {
+                Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}")
+            }
+        } while (readBytes <= 0 && attempts-- > 0)
+        if (inputBuffer[0] != 0x82.toByte()) {
+            throw UsbTransportException(buildString {
+                append("USB-CCID error - bad CCID header")
+                append(", type ")
+                append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK))
+                if (expectedSequenceNumber != inputBuffer[6]) {
+                    append(", sequence number ")
+                    append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
+                }
+            })
+        }
+        return inputBuffer
+    }
+
     private fun receiveDataBlock(expectedSequenceNumber: Byte): CcidDataBlock {
     private fun receiveDataBlock(expectedSequenceNumber: Byte): CcidDataBlock {
         var response: CcidDataBlock?
         var response: CcidDataBlock?
         do {
         do {
@@ -283,6 +323,38 @@ class UsbCcidTransceiver(
         return ccidDataBlock
         return ccidDataBlock
     }
     }
 
 
+    fun sendParamBlock(
+        payload: ByteArray
+    ): ByteArray {
+        val startTime = SystemClock.elapsedRealtime()
+        val l = payload.size
+        val sequenceNumber: Byte = currentSequenceNumber++
+        val headerData = byteArrayOf(
+            0x61.toByte(),
+            l.toByte(),
+            (l shr 8).toByte(),
+            (l shr 16).toByte(),
+            (l shr 24).toByte(),
+            SLOT_NUMBER.toByte(),
+            sequenceNumber,
+            0x00.toByte(),
+            0x00.toByte(),
+            0x00.toByte()
+        )
+        val data: ByteArray = headerData + payload
+        Log.d(TAG, "USB ParamBlock: ${data.encodeHex()}")
+        var sentBytes = 0
+        while (sentBytes < data.size) {
+            val bytesToSend = usbBulkOut.maxPacketSize.coerceAtMost(data.size - sentBytes)
+            sendRaw(data, sentBytes, bytesToSend)
+            sentBytes += bytesToSend
+        }
+        val ccidDataBlock = receiveParamBlock(sequenceNumber)
+        val elapsedTime = SystemClock.elapsedRealtime() - startTime
+        Log.d(TAG, "USB ParamBlock call took ${elapsedTime}ms")
+        return ccidDataBlock
+    }
+
     fun iccPowerOn(): CcidDataBlock {
     fun iccPowerOn(): CcidDataBlock {
         val startTime = SystemClock.elapsedRealtime()
         val startTime = SystemClock.elapsedRealtime()
         skipAvailableInput()
         skipAvailableInput()

+ 9 - 0
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt

@@ -25,3 +25,12 @@ val Iterable<UsbEndpoint>.bulkPair: Pair<UsbEndpoint?, UsbEndpoint?>
             endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
             endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
         )
         )
     }
     }
+
+val KNOWN_TPDU_READERS: Set<Pair<Int, Int>> = setOf(
+    // USB vendor ID + product ID pairs that require TPDU mode
+    // Realtek RTS5169 from <https://gitea.angry.im/PeterCxy/OpenEUICC/issues/37>
+    Pair(0x0bda, 0x0169)
+)
+
+fun isKnownTpduReader(vendorId: Int, productId: Int): Boolean =
+    KNOWN_TPDU_READERS.contains(Pair(vendorId, productId))

+ 3 - 0
app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt

@@ -71,6 +71,9 @@ open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker
         requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
         requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
             .bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
             .bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
 
 
+        requirePreference<CheckBoxPreference>("pref_advanced_force_tpdu_mode")
+            .bindBooleanFlow(preferenceRepository.forceTpduModeFlow)
+
         requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
         requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
             .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
             .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
 
 

+ 2 - 0
app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt

@@ -31,6 +31,7 @@ internal object PreferenceKeys {
     // ---- Advanced ----
     // ---- Advanced ----
     val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
     val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
     val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
     val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
+    val FORCE_TPDU_MODE = booleanPreferencesKey("force_tpdu_mode")
 
 
     // ---- Developer Options ----
     // ---- Developer Options ----
     val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
     val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
@@ -87,6 +88,7 @@ open class PreferenceRepository(private val context: Context) {
     // ---- Advanced ----
     // ---- Advanced ----
     val disableSafeguardFlow = bindFlow(PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM, false)
     val disableSafeguardFlow = bindFlow(PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM, false)
     val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
     val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
+    val forceTpduModeFlow = bindFlow(PreferenceKeys.FORCE_TPDU_MODE, false)
 
 
     // ---- Developer Options ----
     // ---- Developer Options ----
     val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
     val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)

+ 2 - 0
app-common/src/main/res/values-ja/strings.xml

@@ -172,6 +172,8 @@
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと<i>時々</i>アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を<i>解除</i>します。</string>
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと<i>時々</i>アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を<i>解除</i>します。</string>
     <string name="pref_advanced_verbose_logging">詳細ログ</string>
     <string name="pref_advanced_verbose_logging">詳細ログ</string>
     <string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</string>
     <string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</string>
+    <string name="pref_advanced_force_tpdu_mode">USBリーダーでTPDUモードを強制</string>
+    <string name="pref_advanced_force_tpdu_mode_desc">すべてのUSB CCIDリーダーでTPDUモードを強制します。TPDUのみで動作するUSBリーダーがある場合にのみ有効にしてください。</string>
     <string name="pref_advanced_language">言語</string>
     <string name="pref_advanced_language">言語</string>
     <string name="pref_advanced_language_desc">アプリの言語を設定します。</string>
     <string name="pref_advanced_language_desc">アプリの言語を設定します。</string>
     <string name="pref_advanced_logs">ログ</string>
     <string name="pref_advanced_logs">ログ</string>

+ 2 - 0
app-common/src/main/res/values-zh-rCN/strings.xml

@@ -80,6 +80,8 @@
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">默认情况下,此应用程序会阻止您禁用可插拔 eSIM 中已启用的配置文件。\n因为这样做 <i>有时</i> 会使其无法访问。\n勾选此框以 <i>移除</i> 此保护措施。</string>
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">默认情况下,此应用程序会阻止您禁用可插拔 eSIM 中已启用的配置文件。\n因为这样做 <i>有时</i> 会使其无法访问。\n勾选此框以 <i>移除</i> 此保护措施。</string>
     <string name="pref_advanced_verbose_logging">记录详细日志</string>
     <string name="pref_advanced_verbose_logging">记录详细日志</string>
     <string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
     <string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
+    <string name="pref_advanced_force_tpdu_mode">强制 USB 读卡器使用 TPDU 模式</string>
+    <string name="pref_advanced_force_tpdu_mode_desc">强制所有 USB CCID 读卡器使用 TPDU 模式。仅在您的 USB 读卡器仅支持 TPDU 模式时启用。</string>
     <string name="pref_advanced_logs">日志</string>
     <string name="pref_advanced_logs">日志</string>
     <string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
     <string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
     <string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eSIM 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。</string>
     <string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eSIM 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。</string>

+ 2 - 0
app-common/src/main/res/values-zh-rTW/strings.xml

@@ -76,6 +76,8 @@
     <string name="pref_notifications_switch">切換</string>
     <string name="pref_notifications_switch">切換</string>
     <string name="pref_advanced_verbose_logging">記錄詳細日誌</string>
     <string name="pref_advanced_verbose_logging">記錄詳細日誌</string>
     <string name="pref_advanced_verbose_logging_desc">詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。</string>
     <string name="pref_advanced_verbose_logging_desc">詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。</string>
+    <string name="pref_advanced_force_tpdu_mode">強制 USB 讀卡機使用 TPDU 模式</string>
+    <string name="pref_advanced_force_tpdu_mode_desc">強制所有 USB CCID 讀卡機使用 TPDU 模式。僅在您的 USB 讀卡機僅支援 TPDU 模式時啟用。</string>
     <string name="pref_advanced_logs">日誌</string>
     <string name="pref_advanced_logs">日誌</string>
     <string name="pref_advanced_logs_desc">檢視應用程式的最新除錯日誌</string>
     <string name="pref_advanced_logs_desc">檢視應用程式的最新除錯日誌</string>
     <string name="pref_notifications_switch_desc">傳送 <i>切換</i> 設定檔的通知\n注意,這個狀態的通知不一定有用。</string>
     <string name="pref_notifications_switch_desc">傳送 <i>切換</i> 設定檔的通知\n注意,這個狀態的通知不一定有用。</string>

+ 2 - 0
app-common/src/main/res/values/strings.xml

@@ -208,6 +208,8 @@
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may <i>sometimes</i> render it inaccessible.\nCheck this box to <i>remove</i> this safeguard.</string>
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may <i>sometimes</i> render it inaccessible.\nCheck this box to <i>remove</i> this safeguard.</string>
     <string name="pref_advanced_verbose_logging">Verbose Logging</string>
     <string name="pref_advanced_verbose_logging">Verbose Logging</string>
     <string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
     <string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
+    <string name="pref_advanced_force_tpdu_mode">Force TPDU Mode for USB Readers</string>
+    <string name="pref_advanced_force_tpdu_mode_desc">Force TPDU mode for all USB CCID readers. Only enable if you have a USB reader that only works under TPDU.</string>
     <string name="pref_advanced_language">Language</string>
     <string name="pref_advanced_language">Language</string>
     <string name="pref_advanced_language_desc">Select app language</string>
     <string name="pref_advanced_language_desc">Select app language</string>
     <string name="pref_advanced_logs">Logs</string>
     <string name="pref_advanced_logs">Logs</string>

+ 6 - 0
app-common/src/main/res/xml/pref_settings.xml

@@ -37,6 +37,12 @@
             app:summary="@string/pref_advanced_verbose_logging_desc"
             app:summary="@string/pref_advanced_verbose_logging_desc"
             app:title="@string/pref_advanced_verbose_logging" />
             app:title="@string/pref_advanced_verbose_logging" />
 
 
+        <CheckBoxPreference
+            app:iconSpaceReserved="false"
+            app:key="pref_advanced_force_tpdu_mode"
+            app:summary="@string/pref_advanced_force_tpdu_mode_desc"
+            app:title="@string/pref_advanced_force_tpdu_mode" />
+
         <Preference
         <Preference
             app:iconSpaceReserved="false"
             app:iconSpaceReserved="false"
             app:isPreferenceVisible="false"
             app:isPreferenceVisible="false"