ソースを参照

[2/n] USB CCID Reader support

*cough* copied CCID driver from OpenKeychains
Peter Cai 1 年間 前
コミット
3667f578d7

+ 16 - 0
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt

@@ -91,6 +91,10 @@ open class DefaultEuiccChannelManager(
     override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
         runBlocking {
             withContext(Dispatchers.IO) {
+                if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+                    return@withContext usbChannel
+                }
+
                 for (card in uiccCards) {
                     for (port in card.ports) {
                         if (port.logicalSlotIndex == logicalSlotId) {
@@ -106,6 +110,10 @@ open class DefaultEuiccChannelManager(
     override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
         runBlocking {
             withContext(Dispatchers.IO) {
+                if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+                    return@withContext usbChannel
+                }
+
                 for (card in uiccCards) {
                     if (card.physicalSlotIndex != physicalSlotId) continue
                     for (port in card.ports) {
@@ -118,6 +126,10 @@ open class DefaultEuiccChannelManager(
         }
 
     override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
+        if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+            return usbChannel?.let { listOf(it) }
+        }
+
         for (card in uiccCards) {
             if (card.physicalSlotIndex != physicalSlotId) continue
             return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
@@ -133,6 +145,10 @@ open class DefaultEuiccChannelManager(
 
     override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
         withContext(Dispatchers.IO) {
+            if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+                return@withContext usbChannel
+            }
+
             uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
                 card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
             }

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

@@ -2,6 +2,8 @@ package im.angry.openeuicc.core.usb
 
 import android.hardware.usb.UsbDeviceConnection
 import android.hardware.usb.UsbEndpoint
+import android.util.Log
+import im.angry.openeuicc.util.*
 import net.typeblog.lpac_jni.ApduInterface
 
 class UsbApduInterface(
@@ -9,11 +11,30 @@ class UsbApduInterface(
     private val bulkIn: UsbEndpoint,
     private val bulkOut: UsbEndpoint
 ): ApduInterface {
+    companion object {
+        private const val TAG = "UsbApduInterface"
+    }
+
     private lateinit var ccidDescription: UsbCcidDescription
+    private lateinit var transceiver: UsbCcidTransceiver
+
+    private var channelId = -1
 
     override fun connect() {
         ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
-        ccidDescription.checkTransportProtocol()
+
+        if (!ccidDescription.hasT0Protocol) {
+            throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
+        }
+
+        transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription)
+
+        try {
+            transceiver.iccPowerOn()
+        } catch (e: Exception) {
+            e.printStackTrace()
+            throw e
+        }
     }
 
     override fun disconnect() {
@@ -21,17 +42,118 @@ class UsbApduInterface(
     }
 
     override fun logicalChannelOpen(aid: ByteArray): Int {
-        return 0
+        check(channelId == -1) { "Logical channel already opened" }
+
+        // OPEN LOGICAL CHANNEL
+        val req = manageChannelCmd(true, 0)
+        Log.d(TAG, "OPEN LOGICAL CHANNEL: ${req.encodeHex()}")
+
+        val resp = try {
+            transmitApduByChannel(req, 0)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            return -1
+        }
+        Log.d(TAG, "OPEN LOGICAL CHANNEL response: ${resp.encodeHex()}")
+
+        return if (resp.size >= 2 && resp.sliceArray((resp.size - 2) until resp.size).contentEquals(
+                byteArrayOf(0x90.toByte(), 0x00)
+            )
+        ) {
+            channelId = resp[0].toInt()
+            Log.d(TAG, "channelId = $channelId")
+
+            // Then, select AID
+            val selectAid = selectByDfCmd(aid, channelId.toByte())
+            Log.d(TAG, "Select DF command: ${selectAid.encodeHex()}")
+            val selectAidResp = transmitApduByChannel(selectAid, channelId.toByte())
+            Log.d(TAG, "Select DF resp: ${selectAidResp.encodeHex()}")
+            channelId
+        } else {
+            -1
+        }
     }
 
     override fun logicalChannelClose(handle: Int) {
+        check(handle == channelId) { "Logical channel ID mismatch" }
+        check(channelId != -1) { "Logical channel is not opened" }
+
+        // CLOSE LOGICAL CHANNEL
+        val req = manageChannelCmd(false, channelId.toByte())
+        Log.d(TAG, "CLOSE LOGICAL CHANNEL: ${req.encodeHex()}")
+
+        val resp = transmitApduByChannel(req, channelId.toByte())
+        Log.d(TAG, "CLOSE LOGICAL CHANNEL response: ${resp.encodeHex()}")
 
+        channelId = -1
     }
 
     override fun transmit(tx: ByteArray): ByteArray {
-        return byteArrayOf()
+        check(channelId != -1) { "Logical channel is not opened" }
+        Log.d(TAG, "USB APDU command: ${tx.encodeHex()}")
+        val resp = transmitApduByChannel(tx, channelId.toByte())
+        Log.d(TAG, "USB APDU response: ${resp.encodeHex()}")
+        return resp
     }
 
     override val valid: Boolean
         get() = true
+
+    private fun buildCmd(cla: Byte, ins: Byte, p1: Byte, p2: Byte, data: ByteArray?, le: Byte?) =
+        byteArrayOf(cla, ins, p1, p2).let {
+            if (data != null) {
+                it + data.size.toByte() + data
+            } else {
+                it
+            }
+        }.let {
+            if (le != null) {
+                it + byteArrayOf(le)
+            } else {
+                it
+            }
+        }
+
+    private fun manageChannelCmd(open: Boolean, channel: Byte) =
+        if (open) {
+            buildCmd(0x00, 0x70, 0x00, 0x00, null, 0x01)
+        } else {
+            buildCmd(channel, 0x70, 0x80.toByte(), channel, null, null)
+        }
+
+    private fun selectByDfCmd(aid: ByteArray, channel: Byte) =
+        buildCmd(channel, 0xA4.toByte(), 0x04, 0x00, aid, null)
+
+    private fun transmitApduByChannel(tx: ByteArray, channel: Byte): ByteArray {
+        val realTx = tx.copyOf()
+        // OR the channel mask into the CLA byte
+        realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
+
+        var resp = transceiver.sendXfrBlock(realTx)!!.data!!
+
+        if (resp.size >= 2) {
+            var sw1 = resp[resp.size - 2].toInt() and 0xFF
+            var sw2 = resp[resp.size - 1].toInt() and 0xFF
+
+            if (sw1 == 0x6C) {
+                realTx[realTx.size - 1] = resp[resp.size - 1]
+                resp = transceiver.sendXfrBlock(realTx)!!.data!!
+            } else if (sw1 == 0x61) {
+                do {
+                    val getResponseCmd = byteArrayOf(
+                        realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
+                    )
+
+                    val tmp = transceiver.sendXfrBlock(getResponseCmd)!!.data!!
+
+                    resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
+
+                    sw1 = resp[resp.size - 2].toInt() and 0xFF
+                    sw2 = resp[resp.size - 1].toInt() and 0xFF
+                } while (sw1 == 0x61)
+            }
+        }
+
+        return resp
+    }
 }

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

@@ -96,11 +96,9 @@ data class UsbCcidDescription(
                 }.toTypedArray()
             }
 
-    val hasAutomaticPps: Boolean = hasFeature(FEATURE_AUTOMATIC_PPS)
+    val hasAutomaticPps: Boolean
+        get() = hasFeature(FEATURE_AUTOMATIC_PPS)
 
-    fun checkTransportProtocol() {
-        val hasT1Protocol = dwProtocols and MASK_T1_PROTO != 0
-        val hasT0Protocol = dwProtocols and MASK_T0_PROTO != 0
-        android.util.Log.d("CcidDescription", "hasT1Protocol = $hasT1Protocol, hasT0Protocol = $hasT0Protocol")
-    }
+    val hasT0Protocol: Boolean
+        get() = (dwProtocols and MASK_T0_PROTO) != 0
 }

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

@@ -0,0 +1,348 @@
+package im.angry.openeuicc.core.usb
+
+import android.hardware.usb.UsbDeviceConnection
+import android.hardware.usb.UsbEndpoint
+import android.os.SystemClock
+import android.util.Log
+import im.angry.openeuicc.util.*
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+
+/**
+ * Provides raw, APDU-agnostic transmission to the CCID reader
+ * Adapted from <https://github.com/open-keychain/open-keychain/blob/master/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java>
+ */
+class UsbCcidTransceiver(
+    private val usbConnection: UsbDeviceConnection,
+    private val usbBulkIn: UsbEndpoint,
+    private val usbBulkOut: UsbEndpoint,
+    private val usbCcidDescription: UsbCcidDescription
+) {
+    companion object {
+        private const val TAG = "UsbCcidTransceiver"
+
+        private const val CCID_HEADER_LENGTH = 10
+
+        private const val MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK = 0x80
+        private const val MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON = 0x62
+        private const val MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF = 0x63
+        private const val MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK = 0x6f
+
+        private const val COMMAND_STATUS_SUCCESS: Byte = 0
+        private const val COMMAND_STATUS_TIME_EXTENSION_RQUESTED: Byte = 2
+
+        /**
+         * Level Parameter: APDU is a single command.
+         *
+         * "the command APDU begins and ends with this command"
+         * -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
+         * § 6.1.1.3
+         */
+        const val LEVEL_PARAM_START_SINGLE_CMD_APDU: Short = 0x0000
+
+        /**
+         * Level Parameter: First APDU in a multi-command APDU.
+         *
+         * "the command APDU begins with this command, and continue in the
+         * next PC_to_RDR_XfrBlock"
+         * -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
+         * § 6.1.1.3
+         */
+        const val LEVEL_PARAM_START_MULTI_CMD_APDU: Short = 0x0001
+
+        /**
+         * Level Parameter: Final APDU in a multi-command APDU.
+         *
+         * "this abData field continues a command APDU and ends the command APDU"
+         * -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
+         * § 6.1.1.3
+         */
+        const val LEVEL_PARAM_END_MULTI_CMD_APDU: Short = 0x0002
+
+        /**
+         * Level Parameter: Next command in a multi-command APDU.
+         *
+         * "the abData field continues a command APDU and another block is to follow"
+         * -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
+         * § 6.1.1.3
+         */
+        const val LEVEL_PARAM_CONTINUE_MULTI_CMD_APDU: Short = 0x0003
+
+        /**
+         * Level Parameter: Request the device continue sending APDU.
+         *
+         * "empty abData field, continuation of response APDU is expected in the next
+         * RDR_to_PC_DataBlock"
+         * -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
+         * § 6.1.1.3
+         */
+        const val LEVEL_PARAM_CONTINUE_RESPONSE: Short = 0x0010
+
+        private const val SLOT_NUMBER = 0x00
+
+        private const val ICC_STATUS_SUCCESS: Byte = 0
+
+        private const val DEVICE_COMMUNICATE_TIMEOUT_MILLIS = 5000
+        private const val DEVICE_SKIP_TIMEOUT_MILLIS = 100
+    }
+
+    data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
+        Exception(msg)
+
+    data class CcidDataBlock(
+        val dwLength: Int,
+        val bSlot: Byte,
+        val bSeq: Byte,
+        val bStatus: Byte,
+        val bError: Byte,
+        val bChainParameter: Byte,
+        val data: ByteArray?
+    ) {
+        companion object {
+            fun parseHeaderFromBytes(headerBytes: ByteArray): CcidDataBlock {
+                val buf = ByteBuffer.wrap(headerBytes)
+                buf.order(ByteOrder.LITTLE_ENDIAN)
+
+                val type = buf.get()
+                require(type == MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) { "Header has incorrect type value!" }
+                val dwLength = buf.int
+                val bSlot = buf.get()
+                val bSeq = buf.get()
+                val bStatus = buf.get()
+                val bError = buf.get()
+                val bChainParameter = buf.get()
+
+                return CcidDataBlock(dwLength, bSlot, bSeq, bStatus, bError, bChainParameter, null)
+            }
+        }
+
+        fun withData(d: ByteArray): CcidDataBlock {
+            require(data == null) { "Cannot add data twice" }
+            return CcidDataBlock(dwLength, bSlot, bSeq, bStatus, bError, bChainParameter, d)
+        }
+
+        val iccStatus: Byte
+            get() = (bStatus.toInt() and 0x03).toByte()
+
+        val commandStatus: Byte
+            get() = ((bStatus.toInt() shr 6) and 0x03).toByte()
+
+        val isStatusTimeoutExtensionRequest: Boolean
+            get() = commandStatus == COMMAND_STATUS_TIME_EXTENSION_RQUESTED
+
+        val isStatusSuccess: Boolean
+            get() = iccStatus == ICC_STATUS_SUCCESS && commandStatus == COMMAND_STATUS_SUCCESS
+    }
+
+    val hasAutomaticPps = usbCcidDescription.hasAutomaticPps
+
+    private val inputBuffer = ByteArray(usbBulkIn.maxPacketSize)
+
+    private var currentSequenceNumber: Byte = 0
+
+    private fun sendRaw(data: ByteArray, offset: Int, length: Int) {
+        val tr1 = usbConnection.bulkTransfer(
+            usbBulkOut, data, offset, length, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
+        )
+        if (tr1 != length) {
+            throw UsbTransportException(
+                "USB error - failed to transmit data ($tr1/$length)"
+            )
+        }
+    }
+
+    private fun receiveDataBlock(expectedSequenceNumber: Byte): CcidDataBlock? {
+        var response: CcidDataBlock?
+        do {
+            response = receiveDataBlockImmediate(expectedSequenceNumber)
+        } while (response!!.isStatusTimeoutExtensionRequest)
+        if (!response.isStatusSuccess) {
+            throw UsbCcidErrorException("USB-CCID error!", response)
+        }
+        return response
+    }
+
+    private fun receiveDataBlockImmediate(expectedSequenceNumber: Byte): CcidDataBlock? {
+        /*
+         * 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
+            )
+            Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
+        } while (readBytes <= 0 && attempts-- > 0)
+        if (readBytes < CCID_HEADER_LENGTH) {
+            throw UsbTransportException("USB-CCID error - failed to receive CCID header")
+        }
+        if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) {
+            if (expectedSequenceNumber != inputBuffer[6]) {
+                throw UsbTransportException(
+                    ((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
+                            MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
+                            ) + " (expected " +
+                            expectedSequenceNumber + ")"
+                )
+            }
+            throw UsbTransportException(
+                "USB-CCID error - bad CCID header type " + inputBuffer[0]
+            )
+        }
+        var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
+        if (expectedSequenceNumber != result.bSeq) {
+            throw UsbTransportException(
+                ("USB-CCID error - expected sequence number " +
+                        expectedSequenceNumber + ", got " + result)
+            )
+        }
+
+        val dataBuffer = ByteArray(result.dwLength)
+        var bufferedBytes = readBytes - CCID_HEADER_LENGTH
+        System.arraycopy(inputBuffer, CCID_HEADER_LENGTH, dataBuffer, 0, bufferedBytes)
+        while (bufferedBytes < dataBuffer.size) {
+            readBytes = usbConnection.bulkTransfer(
+                usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
+            )
+            if (readBytes < 0) {
+                throw UsbTransportException(
+                    "USB error - failed reading response data! Header: $result"
+                )
+            }
+            System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
+            bufferedBytes += readBytes
+        }
+        result = result.withData(dataBuffer)
+        return result
+    }
+
+
+    private fun skipAvailableInput() {
+        var ignoredBytes: Int
+        do {
+            ignoredBytes = usbConnection.bulkTransfer(
+                usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_SKIP_TIMEOUT_MILLIS
+            )
+            if (ignoredBytes > 0) {
+                Log.e(TAG, "Skipped $ignoredBytes bytes")
+            }
+        } while (ignoredBytes > 0)
+    }
+
+    /**
+     * Transmits XfrBlock
+     * 6.1.4 PC_to_RDR_XfrBlock
+     *
+     * @param payload payload to transmit
+     */
+    fun sendXfrBlock(payload: ByteArray): CcidDataBlock? {
+        return sendXfrBlock(payload, LEVEL_PARAM_START_SINGLE_CMD_APDU)
+    }
+
+    /**
+     * Receives a continued XfrBlock. Should be called when a multiblock response is indicated
+     * 6.1.4 PC_to_RDR_XfrBlock
+     */
+    fun receiveContinuedResponse(): CcidDataBlock? {
+        return sendXfrBlock(ByteArray(0), LEVEL_PARAM_CONTINUE_RESPONSE)
+    }
+
+    /**
+     * Transmits XfrBlock
+     * 6.1.4 PC_to_RDR_XfrBlock
+     *
+     * @param payload payload to transmit
+     * @param levelParam Level parameter
+     */
+    private fun sendXfrBlock(payload: ByteArray, levelParam: Short): CcidDataBlock? {
+        val startTime = SystemClock.elapsedRealtime()
+        val l = payload.size
+        val sequenceNumber: Byte = currentSequenceNumber++
+        val headerData = byteArrayOf(
+            MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK.toByte(),
+            l.toByte(),
+            (l shr 8).toByte(),
+            (l shr 16).toByte(),
+            (l shr 24).toByte(),
+            SLOT_NUMBER.toByte(),
+            sequenceNumber,
+            0x00.toByte(),
+            (levelParam.toInt() and 0x00ff).toByte(),
+            (levelParam.toInt() shr 8).toByte()
+        )
+        val data: ByteArray = headerData + payload
+        var sentBytes = 0
+        while (sentBytes < data.size) {
+            val bytesToSend = Math.min(usbBulkOut.maxPacketSize, data.size - sentBytes)
+            sendRaw(data, sentBytes, bytesToSend)
+            sentBytes += bytesToSend
+        }
+        val ccidDataBlock = receiveDataBlock(sequenceNumber)
+        val elapsedTime = SystemClock.elapsedRealtime() - startTime
+        Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms")
+        return ccidDataBlock
+    }
+
+    fun iccPowerOn(): CcidDataBlock {
+        val startTime = SystemClock.elapsedRealtime()
+        skipAvailableInput()
+        var response: CcidDataBlock? = null
+        for (v in usbCcidDescription.voltages) {
+            Log.v(TAG, "CCID: attempting to power on with voltage $v")
+            response = try {
+                iccPowerOnVoltage(v.powerOnValue)
+            } catch (e: UsbCcidErrorException) {
+                if (e.errorResponse.bError.toInt() == 7) { // Power select error
+                    Log.v(TAG, "CCID: failed to power on with voltage $v")
+                    iccPowerOff()
+                    Log.v(TAG, "CCID: powered off")
+                    continue
+                }
+                throw e
+            }
+            break
+        }
+        if (response == null) {
+            throw UsbTransportException("Couldn't power up ICC2")
+        }
+        val elapsedTime = SystemClock.elapsedRealtime() - startTime
+        Log.d(
+            TAG,
+            "Usb transport connected, took " + elapsedTime + "ms, ATR=" +
+                    response.data?.encodeHex()
+        )
+        return response
+    }
+
+    private fun iccPowerOnVoltage(voltage: Byte): CcidDataBlock? {
+        val sequenceNumber = currentSequenceNumber++
+        val iccPowerCommand = byteArrayOf(
+            MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON.toByte(),
+            0x00, 0x00, 0x00, 0x00,
+            SLOT_NUMBER.toByte(),
+            sequenceNumber,
+            voltage,
+            0x00, 0x00 // reserved for future use
+        )
+        sendRaw(iccPowerCommand, 0, iccPowerCommand.size)
+        return receiveDataBlock(sequenceNumber)
+    }
+
+    private fun iccPowerOff() {
+        val sequenceNumber = currentSequenceNumber++
+        val iccPowerCommand = byteArrayOf(
+            MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF.toByte(),
+            0x00, 0x00, 0x00, 0x00,
+            0x00,
+            sequenceNumber,
+            0x00
+        )
+        sendRaw(iccPowerCommand, 0, iccPowerCommand.size)
+    }
+}

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

@@ -6,6 +6,8 @@ import android.hardware.usb.UsbDevice
 import android.hardware.usb.UsbEndpoint
 import android.hardware.usb.UsbInterface
 
+class UsbTransportException(msg: String) : Exception(msg)
+
 fun UsbInterface.getIoEndpoints(): Pair<UsbEndpoint?, UsbEndpoint?> {
     var bulkIn: UsbEndpoint? = null
     var bulkOut: UsbEndpoint? = null

+ 4 - 3
app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt

@@ -74,11 +74,12 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
 }
 
 // Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat
+// except when it is from a USB card reader
 val EuiccChannel.removable
-    get() = (port as RealUiccPortInfoCompat).card.isRemovable
+    get() = (port as? RealUiccPortInfoCompat)?.card?.isRemovable ?: true
 
 val EuiccChannel.cardId
-    get() = (port as RealUiccPortInfoCompat).card.cardId
+    get() = (port as? RealUiccPortInfoCompat)?.card?.cardId ?: -1
 
 val EuiccChannel.isMEP
-    get() = (port as RealUiccPortInfoCompat).card.isMultipleEnabledProfilesSupported
+    get() = (port as? RealUiccPortInfoCompat)?.card?.isMultipleEnabledProfilesSupported ?: false