浏览代码

feat: supports for multi logical channel (#148)

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/148
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
septs 11 月之前
父节点
当前提交
c8ecdee095

+ 6 - 0
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt

@@ -1,6 +1,7 @@
 package im.angry.openeuicc.core
 
 import im.angry.openeuicc.util.*
+import net.typeblog.lpac_jni.ApduInterface
 import net.typeblog.lpac_jni.LocalProfileAssistant
 
 interface EuiccChannel {
@@ -28,5 +29,10 @@ interface EuiccChannel {
      */
     val intrinsicChannelName: String?
 
+    /**
+     * The underlying APDU interface for this channel
+     */
+    val apduInterface: ApduInterface
+
     fun close()
 }

+ 3 - 2
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt

@@ -1,6 +1,7 @@
 package im.angry.openeuicc.core
 
-import im.angry.openeuicc.util.*
+import im.angry.openeuicc.util.UiccPortInfoCompat
+import im.angry.openeuicc.util.decodeHex
 import kotlinx.coroutines.flow.Flow
 import net.typeblog.lpac_jni.ApduInterface
 import net.typeblog.lpac_jni.LocalProfileAssistant
@@ -11,7 +12,7 @@ class EuiccChannelImpl(
     override val type: String,
     override val port: UiccPortInfoCompat,
     override val intrinsicChannelName: String?,
-    private val apduInterface: ApduInterface,
+    override val apduInterface: ApduInterface,
     verboseLoggingFlow: Flow<Boolean>,
     ignoreTLSCertificateFlow: Flow<Boolean>
 ) : EuiccChannel {

+ 3 - 0
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt

@@ -1,6 +1,7 @@
 package im.angry.openeuicc.core
 
 import im.angry.openeuicc.util.*
+import net.typeblog.lpac_jni.ApduInterface
 import net.typeblog.lpac_jni.LocalProfileAssistant
 
 class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
@@ -33,6 +34,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
         get() = channel.valid
     override val intrinsicChannelName: String?
         get() = channel.intrinsicChannelName
+    override val apduInterface: ApduInterface
+        get() = channel.apduInterface
     override val atr: ByteArray?
         get() = channel.atr
 

+ 20 - 16
app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt

@@ -7,7 +7,6 @@ import android.util.Log
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.single
 import kotlinx.coroutines.runBlocking
 import net.typeblog.lpac_jni.ApduInterface
 
@@ -21,7 +20,12 @@ class OmapiApduInterface(
     }
 
     private lateinit var session: Session
-    private lateinit var lastChannel: Channel
+    private val channels = arrayOf<Channel?>(
+        null,
+        null,
+        null,
+        null,
+    )
 
     override val valid: Boolean
         get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@@ -38,24 +42,24 @@ class OmapiApduInterface(
     }
 
     override fun logicalChannelOpen(aid: ByteArray): Int {
-        check(!this::lastChannel.isInitialized) {
-            "Can only open one channel"
-        }
-        lastChannel = session.openLogicalChannel(aid)!!
-        return 1
+        val channel = session.openLogicalChannel(aid)
+        check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
+        val index = channels.indexOf(null)
+        check(index != -1) { "No free logical channel slots" }
+        synchronized(channels) { channels[index] = channel }
+        return index
     }
 
     override fun logicalChannelClose(handle: Int) {
-        check(handle == 1 && !this::lastChannel.isInitialized) {
-            "Unknown channel"
-        }
-        lastChannel.close()
+        val channel = channels.getOrNull(handle)
+        check(channel != null) { "Invalid logical channel handle $handle" }
+        if (channel.isOpen) channel.close()
+        synchronized(channels) { channels[handle] = null }
     }
 
-    override fun transmit(tx: ByteArray): ByteArray {
-        check(this::lastChannel.isInitialized) {
-            "Unknown channel"
-        }
+    override fun transmit(handle: Int, tx: ByteArray): ByteArray {
+        val channel = channels.getOrNull(handle)
+        check(channel != null) { "Invalid logical channel handle $handle" }
 
         if (runBlocking { verboseLoggingFlow.first() }) {
             Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
@@ -63,7 +67,7 @@ class OmapiApduInterface(
 
         try {
             for (i in 0..10) {
-                val res = lastChannel.transmit(tx)
+                val res = channel.transmit(tx)
                 if (runBlocking { verboseLoggingFlow.first() }) {
                     Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
                 }

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

@@ -21,10 +21,13 @@ class UsbApduInterface(
     private lateinit var ccidDescription: UsbCcidDescription
     private lateinit var transceiver: UsbCcidTransceiver
 
-    private var channelId = -1
-
     override var atr: ByteArray? = null
 
+    override val valid: Boolean
+        get() = channels.isNotEmpty()
+
+    private var channels = mutableSetOf<Int>()
+
     override fun connect() {
         ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
 
@@ -46,11 +49,11 @@ class UsbApduInterface(
 
     override fun disconnect() {
         conn.close()
+
+        atr = null
     }
 
     override fun logicalChannelOpen(aid: ByteArray): Int {
-        check(channelId == -1) { "Logical channel already opened" }
-
         // OPEN LOGICAL CHANNEL
         val req = manageChannelCmd(true, 0)
 
@@ -66,7 +69,7 @@ class UsbApduInterface(
             return -1
         }
 
-        channelId = resp[0].toInt()
+        val channelId = resp[0].toInt()
         Log.d(TAG, "channelId = $channelId")
 
         // Then, select AID
@@ -78,32 +81,32 @@ class UsbApduInterface(
             return -1
         }
 
+        channels.add(channelId)
+
         return channelId
     }
 
     override fun logicalChannelClose(handle: Int) {
-        check(handle == channelId) { "Logical channel ID mismatch" }
-        check(channelId != -1) { "Logical channel is not opened" }
-
+        check(channels.contains(handle)) {
+            "Invalid logical channel handle $handle"
+        }
         // CLOSE LOGICAL CHANNEL
-        val req = manageChannelCmd(false, channelId.toByte())
-        val resp = transmitApduByChannel(req, channelId.toByte())
+        val req = manageChannelCmd(false, handle.toByte())
+        val resp = transmitApduByChannel(req, handle.toByte())
 
         if (!isSuccessResponse(resp)) {
             Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
         }
-
-        channelId = -1
+        channels.remove(handle)
     }
 
-    override fun transmit(tx: ByteArray): ByteArray {
-        check(channelId != -1) { "Logical channel is not opened" }
-        return transmitApduByChannel(tx, channelId.toByte())
+    override fun transmit(handle: Int, tx: ByteArray): ByteArray {
+        check(channels.contains(handle)) {
+            "Invalid logical channel handle $handle"
+        }
+        return transmitApduByChannel(tx, handle.toByte())
     }
 
-    override val valid: Boolean
-        get() = channelId != -1
-
     private fun isSuccessResponse(resp: ByteArray): Boolean =
         resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
 

+ 21 - 36
app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt

@@ -18,12 +18,10 @@ class TelephonyManagerApduInterface(
         const val TAG = "TelephonyManagerApduInterface"
     }
 
-    private var lastChannel: Int = -1
-
     override val valid: Boolean
-        // TelephonyManager channels will never become truly "invalid",
-        // just that transactions might return errors or nonsense
-        get() = lastChannel != -1
+        get() = channels.isNotEmpty()
+
+    private var channels = mutableSetOf<Int>()
 
     override fun connect() {
         // Do nothing
@@ -31,52 +29,39 @@ class TelephonyManagerApduInterface(
 
     override fun disconnect() {
         // Do nothing
-        lastChannel = -1
     }
 
     override fun logicalChannelOpen(aid: ByteArray): Int {
-        check(lastChannel == -1) { "Already initialized" }
         val hex = aid.encodeHex()
         val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
         if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
-            throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
+            throw IllegalArgumentException("Cannot open logical channel $hex via TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
         }
-        lastChannel = channel.channel
-        return lastChannel
+        channels.add(channel.channel)
+        return channel.channel
     }
 
     override fun logicalChannelClose(handle: Int) {
-        check(handle == lastChannel) { "Invalid channel handle " }
+        check(channels.contains(handle)) {
+            "Invalid logical channel handle $handle"
+        }
         tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
-        lastChannel = -1
+        channels.remove(handle)
     }
 
-    override fun transmit(tx: ByteArray): ByteArray {
-        check(lastChannel != -1) { "Uninitialized" }
-
+    override fun transmit(handle: Int, tx: ByteArray): ByteArray {
+        check(channels.contains(handle)) {
+            "Invalid logical channel handle $handle"
+        }
         if (runBlocking { verboseLoggingFlow.first() }) {
             Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
         }
-
-        val cla = tx[0].toUByte().toInt()
-        val instruction = tx[1].toUByte().toInt()
-        val p1 = tx[2].toUByte().toInt()
-        val p2 = tx[3].toUByte().toInt()
-        val p3 = tx[4].toUByte().toInt()
-        val p4 = tx.drop(5).toByteArray().encodeHex()
-
-        return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
-            cla,
-            instruction,
-            p1,
-            p2,
-            p3,
-            p4
-        ).also {
-            if (runBlocking { verboseLoggingFlow.first() }) {
-                Log.d(TAG, "TelephonyManager APDU response: $it")
-            }
-        }?.decodeHex() ?: byteArrayOf()
+        val result = tm.iccTransmitApduLogicalChannelByPortCompat(
+            port.card.physicalSlotIndex, port.portIndex, handle,
+            tx,
+        )
+        if (runBlocking { verboseLoggingFlow.first() })
+            Log.d(TAG, "TelephonyManager APDU response: $result")
+        return result?.decodeHex() ?: byteArrayOf()
     }
-
 }

+ 17 - 6
app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt

@@ -111,15 +111,26 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat(
     }
 
 fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
-    slotIndex: Int, portIndex: Int, channel: Int,
-    cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String?
-): String? =
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+    slotIndex: Int,
+    portIndex: Int,
+    channel: Int,
+    tx: ByteArray
+): String? {
+    val cla = tx[0].toUByte().toInt()
+    val ins = tx[1].toUByte().toInt()
+    val p1 = tx[2].toUByte().toInt()
+    val p2 = tx[3].toUByte().toInt()
+    val p3 = tx[4].toUByte().toInt()
+    val p4 = tx.drop(5).toByteArray().encodeHex()
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
         iccTransmitApduLogicalChannelByPort(
-            slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data
+            slotIndex, portIndex, channel,
+            cla, ins, p1, p2, p3, p4
         )
     } else {
         iccTransmitApduLogicalChannelBySlot(
-            slotIndex, channel, cla, inst, p1, p2, p3, data
+            slotIndex, channel,
+            cla, ins, p1, p2, p3, p4
         )
     }
+}

+ 11 - 2
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt

@@ -8,7 +8,7 @@ interface ApduInterface {
     fun disconnect()
     fun logicalChannelOpen(aid: ByteArray): Int
     fun logicalChannelClose(handle: Int)
-    fun transmit(tx: ByteArray): ByteArray
+    fun transmit(handle: Int, tx: ByteArray): ByteArray
 
     /**
      * Is this APDU connection still valid?
@@ -16,4 +16,13 @@ interface ApduInterface {
      * callers should further check with the LPA to fully determine the validity of a channel
      */
     val valid: Boolean
-}
+
+    fun <T> withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T {
+        val handle = logicalChannelOpen(aid)
+        return try {
+            cb { transmit(handle, it) }
+        } finally {
+            logicalChannelClose(handle)
+        }
+    }
+}

+ 2 - 2
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt

@@ -28,9 +28,9 @@ class LocalProfileAssistantImpl(
         var lastApduResponse: ByteArray? = null
         var lastApduException: Exception? = null
 
-        override fun transmit(tx: ByteArray): ByteArray =
+        override fun transmit(handle: Int, tx: ByteArray): ByteArray =
             try {
-                apduInterface.transmit(tx).also {
+                apduInterface.transmit(handle, tx).also {
                     lastApduException = null
                     lastApduResponse = it
                 }

+ 11 - 5
libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c

@@ -22,7 +22,7 @@ void interface_wrapper_init() {
                                                            "([B)I");
     method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
                                                             "(I)V");
-    method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
+    method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "(I[B)[B");
 
     jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
     method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
@@ -53,24 +53,30 @@ apdu_interface_logical_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, u
     jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
                                      method_apdu_logical_channel_open, jbarr);
     LPAC_JNI_EXCEPTION_RETURN;
+    LPAC_JNI_CTX(ctx)->logical_channel_id = ret;
     return ret;
 }
 
-static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
+static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx,
+                                                 __attribute__((unused)) uint8_t channel) {
     LPAC_JNI_SETUP_ENV;
+    jint logical_channel_id = LPAC_JNI_CTX(ctx)->logical_channel_id;
     (*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
-                           method_apdu_logical_channel_close, channel);
+                           method_apdu_logical_channel_close, logical_channel_id);
     (*env)->ExceptionClear(env);
 }
 
 static int
 apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
                         uint32_t tx_len) {
+    const int logic_channel = LPAC_JNI_CTX(ctx)->logical_channel_id;
     LPAC_JNI_SETUP_ENV;
     jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
     (*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
-    jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
-                                                           method_apdu_transmit, txArr);
+    jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(
+            env, LPAC_JNI_CTX(ctx)->apdu_interface,
+            method_apdu_transmit, logic_channel, txArr
+    );
     LPAC_JNI_EXCEPTION_RETURN;
     *rx_len = (*env)->GetArrayLength(env, ret);
     *rx = calloc(*rx_len, sizeof(uint8_t));

+ 1 - 1
libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c

@@ -28,7 +28,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
     string_constructor = (*env)->GetMethodID(env, string_class, "<init>",
                                              "([BLjava/lang/String;)V");
 
-    const char _unused[1];
+    const jchar _unused[1];
     empty_string = (*env)->NewString(env, _unused, 0);
     empty_string = (*env)->NewGlobalRef(env, empty_string);
 

+ 1 - 0
libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h

@@ -8,6 +8,7 @@ _Static_assert(sizeof(void *) <= sizeof(jlong),
                "jlong must be big enough to hold a platform raw pointer");
 
 struct lpac_jni_ctx {
+    jint logical_channel_id;
     jobject apdu_interface;
     jobject http_interface;
 };