diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 5f399ea..597a70d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/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() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 3da829a..a56b1cc 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/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, ignoreTLSCertificateFlow: Flow ) : EuiccChannel { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index 4204e82..09004d3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/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 diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index c70669d..b3f42b5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/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( + 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()}") } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt index 624ef89..f9e764b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt +++ b/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() + 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() diff --git a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt index 6b09368..f0b1909 100644 --- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt +++ b/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() 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() } - } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt index dbd39f2..a9df18e 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt +++ b/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 ) } +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt index dfa92df..75a6905 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt +++ b/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 -} \ No newline at end of file + + fun withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T { + val handle = logicalChannelOpen(aid) + return try { + cb { transmit(handle, it) } + } finally { + logicalChannelClose(handle) + } + } +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 8aafe94..faf5312 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/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 } diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c b/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c index 9059171..a61fc96 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c +++ b/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)); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 6ea9d3e..ca319db 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/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, "", "([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); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h index a5d6262..c2300be 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h +++ b/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; };