Browse Source

Support TPDU-based CCID readers (#295)

resolves #37

For TPDU based readers like USB 2.0-CRW 2 additional commands are needed in initialisation. Add them

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/295
Reviewed-by: septs <github@septs.pw>
Co-authored-by: Vladimir Serbinenko <phcoder@gmail.com>
Co-committed-by: Vladimir Serbinenko <phcoder@gmail.com>
Vladimir Serbinenko 1 month ago
parent
commit
2cf2d9490a

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

@@ -21,9 +21,70 @@ class UsbApduInterface(
 
     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() {
         ccidCtx.connect()
 
+        if (ccidCtx.transceiver.isTpdu) {
+            // 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
         // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
         val terminalCapabilities = buildCmd(

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

@@ -84,6 +84,8 @@ data class UsbCcidDescription(
 
     private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0
 
+    val isTpdu = hasFeature(0x10000)
+
     val voltages: List<Voltage>
         get() {
             if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO)
@@ -95,4 +97,4 @@ data class UsbCcidDescription(
 
     val hasT0Protocol: Boolean
         get() = (dwProtocols and MASK_T0_PROTO) != 0
-}
+}

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

@@ -143,6 +143,8 @@ class UsbCcidTransceiver(
 
     val hasAutomaticPps = usbCcidDescription.hasAutomaticPps
 
+    val isTpdu = usbCcidDescription.isTpdu
+
     private val inputBuffer = ByteArray(usbBulkIn.maxPacketSize)
 
     private var currentSequenceNumber: Byte = 0
@@ -158,6 +160,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 {
         var response: CcidDataBlock?
         do {
@@ -283,6 +325,38 @@ class UsbCcidTransceiver(
         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 {
         val startTime = SystemClock.elapsedRealtime()
         skipAvailableInput()