Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2cf2d949 authored by Vladimir Serbinenko's avatar Vladimir Serbinenko Committed by Peter Cai
Browse files

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: default avatarsepts <github@septs.pw>
Co-authored-by: default avatarVladimir Serbinenko <phcoder@gmail.com>
Co-committed-by: default avatarVladimir Serbinenko <phcoder@gmail.com>
parent b9863e2e
Loading
Loading
Loading
Loading
+61 −0
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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)
+74 −0
Original line number Diff line number Diff line
@@ -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()