ソースを参照

Avoid reconnecting to USB iface repeatedly while trying different AIDs

Peter Cai 10 ヶ月 前
コミット
1fda120459

+ 4 - 20
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt

@@ -1,25 +1,17 @@
 package im.angry.openeuicc.core
 package im.angry.openeuicc.core
 
 
 import android.content.Context
 import android.content.Context
-import android.hardware.usb.UsbDevice
-import android.hardware.usb.UsbInterface
-import android.hardware.usb.UsbManager
 import android.se.omapi.SEService
 import android.se.omapi.SEService
 import android.util.Log
 import android.util.Log
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.core.usb.UsbApduInterface
 import im.angry.openeuicc.core.usb.UsbApduInterface
-import im.angry.openeuicc.core.usb.bulkPair
-import im.angry.openeuicc.core.usb.endpoints
+import im.angry.openeuicc.core.usb.UsbCcidContext
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 import java.lang.IllegalArgumentException
 import java.lang.IllegalArgumentException
 
 
 open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
 open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
     private var seService: SEService? = null
     private var seService: SEService? = null
 
 
-    private val usbManager by lazy {
-        context.getSystemService(Context.USB_SERVICE) as UsbManager
-    }
-
     private suspend fun ensureSEService() {
     private suspend fun ensureSEService() {
         if (seService == null || !seService!!.isConnected) {
         if (seService == null || !seService!!.isConnected) {
             seService = connectSEService(context)
             seService = connectSEService(context)
@@ -72,24 +64,16 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
     }
     }
 
 
     override fun tryOpenUsbEuiccChannel(
     override fun tryOpenUsbEuiccChannel(
-        usbDevice: UsbDevice,
-        usbInterface: UsbInterface,
+        ccidCtx: UsbCcidContext,
         isdrAid: ByteArray
         isdrAid: ByteArray
     ): EuiccChannel? {
     ): EuiccChannel? {
-        val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
-        if (bulkIn == null || bulkOut == null) return null
-        val conn = usbManager.openDevice(usbDevice) ?: return null
-        if (!conn.claimInterface(usbInterface, true)) return null
         try {
         try {
             return EuiccChannelImpl(
             return EuiccChannelImpl(
                 context.getString(R.string.usb),
                 context.getString(R.string.usb),
                 FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
                 FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
-                intrinsicChannelName = usbDevice.productName,
+                intrinsicChannelName = ccidCtx.productName,
                 UsbApduInterface(
                 UsbApduInterface(
-                    conn,
-                    bulkIn,
-                    bulkOut,
-                    context.preferenceRepository.verboseLoggingFlow
+                    ccidCtx
                 ),
                 ),
                 isdrAid,
                 isdrAid,
                 context.preferenceRepository.verboseLoggingFlow,
                 context.preferenceRepository.verboseLoggingFlow,

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

@@ -5,6 +5,7 @@ import android.hardware.usb.UsbDevice
 import android.hardware.usb.UsbManager
 import android.hardware.usb.UsbManager
 import android.telephony.SubscriptionManager
 import android.telephony.SubscriptionManager
 import android.util.Log
 import android.util.Log
+import im.angry.openeuicc.core.usb.UsbCcidContext
 import im.angry.openeuicc.core.usb.smartCard
 import im.angry.openeuicc.core.usb.smartCard
 import im.angry.openeuicc.core.usb.interfaces
 import im.angry.openeuicc.core.usb.interfaces
 import im.angry.openeuicc.di.AppContainer
 import im.angry.openeuicc.di.AppContainer
@@ -275,11 +276,15 @@ open class DefaultEuiccChannelManager(
                     TAG,
                     TAG,
                     "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
                     "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
                 )
                 )
+
+                val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
+
                 try {
                 try {
                     val channel = tryOpenChannelFirstValidAid {
                     val channel = tryOpenChannelFirstValidAid {
-                        euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface, it)
+                        euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
                     }
                     }
                     if (channel != null && channel.lpa.valid) {
                     if (channel != null && channel.lpa.valid) {
+                        ccidCtx.allowDisconnect = true
                         usbChannel = channel
                         usbChannel = channel
                         return@withContext Pair(device, true)
                         return@withContext Pair(device, true)
                     }
                     }
@@ -287,6 +292,10 @@ open class DefaultEuiccChannelManager(
                     // Ignored -- skip forward
                     // Ignored -- skip forward
                     e.printStackTrace()
                     e.printStackTrace()
                 }
                 }
+
+                ccidCtx.allowDisconnect = true
+                ccidCtx.disconnect()
+
                 Log.i(
                 Log.i(
                     TAG,
                     TAG,
                     "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"
                     "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"

+ 2 - 4
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt

@@ -1,7 +1,6 @@
 package im.angry.openeuicc.core
 package im.angry.openeuicc.core
 
 
-import android.hardware.usb.UsbDevice
-import android.hardware.usb.UsbInterface
+import im.angry.openeuicc.core.usb.UsbCcidContext
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 
 
 // This class is here instead of inside DI because it contains a bit more logic than just
 // This class is here instead of inside DI because it contains a bit more logic than just
@@ -10,8 +9,7 @@ interface EuiccChannelFactory {
     suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
     suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
 
 
     fun tryOpenUsbEuiccChannel(
     fun tryOpenUsbEuiccChannel(
-        usbDevice: UsbDevice,
-        usbInterface: UsbInterface,
+        ccidCtx: UsbCcidContext,
         isdrAid: ByteArray
         isdrAid: ByteArray
     ): EuiccChannel?
     ): EuiccChannel?
 
 

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

@@ -1,27 +1,19 @@
 package im.angry.openeuicc.core.usb
 package im.angry.openeuicc.core.usb
 
 
-import android.hardware.usb.UsbDeviceConnection
-import android.hardware.usb.UsbEndpoint
 import android.util.Log
 import android.util.Log
 import im.angry.openeuicc.core.ApduInterfaceAtrProvider
 import im.angry.openeuicc.core.ApduInterfaceAtrProvider
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
-import kotlinx.coroutines.flow.Flow
 import net.typeblog.lpac_jni.ApduInterface
 import net.typeblog.lpac_jni.ApduInterface
 
 
 class UsbApduInterface(
 class UsbApduInterface(
-    private val conn: UsbDeviceConnection,
-    private val bulkIn: UsbEndpoint,
-    private val bulkOut: UsbEndpoint,
-    private val verboseLoggingFlow: Flow<Boolean>
+    private val ccidCtx: UsbCcidContext
 ) : ApduInterface, ApduInterfaceAtrProvider {
 ) : ApduInterface, ApduInterfaceAtrProvider {
     companion object {
     companion object {
         private const val TAG = "UsbApduInterface"
         private const val TAG = "UsbApduInterface"
     }
     }
 
 
-    private lateinit var ccidDescription: UsbCcidDescription
-    private lateinit var transceiver: UsbCcidTransceiver
-
-    override var atr: ByteArray? = null
+    override val atr: ByteArray?
+        get() = ccidCtx.atr
 
 
     override val valid: Boolean
     override val valid: Boolean
         get() = channels.isNotEmpty()
         get() = channels.isNotEmpty()
@@ -29,22 +21,7 @@ class UsbApduInterface(
     private var channels = mutableSetOf<Int>()
     private var channels = mutableSetOf<Int>()
 
 
     override fun connect() {
     override fun connect() {
-        ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
-
-        if (!ccidDescription.hasT0Protocol) {
-            throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
-        }
-
-        transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
-
-        try {
-            // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40)
-            // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf
-            atr = transceiver.iccPowerOn().data
-        } catch (e: Exception) {
-            e.printStackTrace()
-            throw e
-        }
+        ccidCtx.connect()
 
 
         // 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
@@ -56,11 +33,7 @@ class UsbApduInterface(
         transmitApduByChannel(terminalCapabilities, 0)
         transmitApduByChannel(terminalCapabilities, 0)
     }
     }
 
 
-    override fun disconnect() {
-        conn.close()
-
-        atr = null
-    }
+    override fun disconnect() = ccidCtx.disconnect()
 
 
     override fun logicalChannelOpen(aid: ByteArray): Int {
     override fun logicalChannelOpen(aid: ByteArray): Int {
         // OPEN LOGICAL CHANNEL
         // OPEN LOGICAL CHANNEL
@@ -149,7 +122,7 @@ class UsbApduInterface(
         // OR the channel mask into the CLA byte
         // OR the channel mask into the CLA byte
         realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
         realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
 
 
-        var resp = transceiver.sendXfrBlock(realTx).data!!
+        var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
 
 
         if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
         if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
 
 
@@ -160,7 +133,7 @@ class UsbApduInterface(
             // 0x6C = wrong le
             // 0x6C = wrong le
             // so we fix the le field here
             // so we fix the le field here
             realTx[realTx.size - 1] = resp[resp.size - 1]
             realTx[realTx.size - 1] = resp[resp.size - 1]
-            resp = transceiver.sendXfrBlock(realTx).data!!
+            resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
         } else if (sw1 == 0x61) {
         } else if (sw1 == 0x61) {
             // 0x61 = X bytes available
             // 0x61 = X bytes available
             // continue reading by GET RESPONSE
             // continue reading by GET RESPONSE
@@ -170,7 +143,7 @@ class UsbApduInterface(
                     realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
                     realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
                 )
                 )
 
 
-                val tmp = transceiver.sendXfrBlock(getResponseCmd).data!!
+                val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!!
 
 
                 resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
                 resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
 
 

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

@@ -0,0 +1,87 @@
+package im.angry.openeuicc.core.usb
+
+import android.content.Context
+import android.hardware.usb.UsbDevice
+import android.hardware.usb.UsbDeviceConnection
+import android.hardware.usb.UsbEndpoint
+import android.hardware.usb.UsbInterface
+import android.hardware.usb.UsbManager
+import im.angry.openeuicc.util.preferenceRepository
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A wrapper over an usb device + interface, manages the lifecycle independent
+ * of the APDU interface exposed to lpac-jni.
+ *
+ * This allows us to try multiple AIDs on each interface without opening / closing
+ * the USB connection numerous times.
+ */
+class UsbCcidContext private constructor(
+    private val conn: UsbDeviceConnection,
+    private val bulkIn: UsbEndpoint,
+    private val bulkOut: UsbEndpoint,
+    val productName: String,
+    val verboseLoggingFlow: Flow<Boolean>
+) {
+    companion object {
+        fun createFromUsbDevice(
+            context: Context,
+            usbDevice: UsbDevice,
+            usbInterface: UsbInterface
+        ): UsbCcidContext? = runCatching {
+            val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
+            if (bulkIn == null || bulkOut == null) return@runCatching null
+            val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice)
+                ?: return@runCatching null
+            if (!conn.claimInterface(usbInterface, true)) return@runCatching null
+            UsbCcidContext(
+                conn,
+                bulkIn,
+                bulkOut,
+                usbDevice.productName ?: "USB",
+                context.preferenceRepository.verboseLoggingFlow
+            )
+        }.getOrNull()
+    }
+
+    /**
+     * When set to false (the default), the disconnect() method does nothing.
+     * This allows the separation of device disconnection from lpac-jni's APDU interface.
+     */
+    var allowDisconnect = false
+    private var initialized = false
+    lateinit var transceiver: UsbCcidTransceiver
+    var atr: ByteArray? = null
+
+    fun connect() {
+        if (initialized) {
+            return
+        }
+
+        val ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
+
+        if (!ccidDescription.hasT0Protocol) {
+            throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
+        }
+
+        transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
+
+        try {
+            // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40)
+            // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf
+            atr = transceiver.iccPowerOn().data
+        } catch (e: Exception) {
+            e.printStackTrace()
+            throw e
+        }
+
+        initialized = true
+    }
+
+    fun disconnect() {
+        if (initialized && allowDisconnect) {
+            conn.close()
+            atr = null
+        }
+    }
+}