From 31a905436e0684348ba85bac2dfc2c56dc1ec96d Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 25 Feb 2025 10:53:19 +0800 Subject: [PATCH 01/13] feat: detect use product --- .../core/DefaultEuiccChannelFactory.kt | 5 +- .../core/DefaultEuiccChannelManager.kt | 5 +- .../im/angry/openeuicc/core/EuiccChannel.kt | 7 +++ .../angry/openeuicc/core/EuiccChannelImpl.kt | 5 +- .../openeuicc/core/EuiccChannelWrapper.kt | 22 +------ .../openeuicc/core/OmapiApduInterface.kt | 31 +++++----- .../openeuicc/core/usb/UsbApduInterface.kt | 39 +++++++------ .../openeuicc/core/usb/UsbCcidDescription.kt | 34 ++++++----- .../openeuicc/core/usb/UsbCcidTransceiver.kt | 41 +++++++------ .../angry/openeuicc/core/usb/UsbCcidUtils.kt | 41 ++++++------- .../angry/openeuicc/ui/EuiccInfoActivity.kt | 43 +++++++------- .../im/angry/openeuicc/util/StringUtils.kt | 9 +-- .../im/angry/openeuicc/vendored/estkme.kt | 36 ++++++++++++ .../im/angry/openeuicc/vendored/simlink.kt | 19 +++++++ app-common/src/main/res/values/strings.xml | 5 ++ .../core/TelephonyManagerApduInterface.kt | 57 +++++++------------ .../util/PrivilegedTelephonyCompat.kt | 23 ++++++-- .../net/typeblog/lpac_jni/ApduInterface.kt | 17 +++++- .../java/net/typeblog/lpac_jni/EuiccInfo2.kt | 33 ++++++++--- .../impl/LocalProfileAssistantImpl.kt | 23 ++++---- .../lpac_jni/impl/RootCertificates.kt | 4 +- .../src/main/jni/lpac-jni/interface-wrapper.c | 9 ++- 22 files changed, 288 insertions(+), 220 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 5e87564..ea0bd60 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -8,7 +8,8 @@ import android.se.omapi.SEService import android.util.Log import im.angry.openeuicc.common.R import im.angry.openeuicc.core.usb.UsbApduInterface -import im.angry.openeuicc.core.usb.getIoEndpoints +import im.angry.openeuicc.core.usb.bulkPair +import im.angry.openeuicc.core.usb.endpoints import im.angry.openeuicc.util.* import java.lang.IllegalArgumentException @@ -61,7 +62,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha } override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? { - val (bulkIn, bulkOut) = usbInterface.getIoEndpoints() + 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 diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 293042c..dd57eab 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -5,7 +5,8 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.telephony.SubscriptionManager import android.util.Log -import im.angry.openeuicc.core.usb.getSmartCardInterface +import im.angry.openeuicc.core.usb.smartCard +import im.angry.openeuicc.core.usb.interfaces import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -244,7 +245,7 @@ open class DefaultEuiccChannelManager( withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") - val iface = device.getSmartCardInterface() ?: return@forEach + val iface = device.interfaces.smartCard ?: return@forEach // If we don't have permission, tell UI code that we found a candidate device, but we // need permission to be able to do anything with it if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) 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..0ecca21 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,8 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* +import im.angry.openeuicc.vendored.ESTKmeInfo +import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant interface EuiccChannel { @@ -28,5 +30,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..1a108e5 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,9 +1,8 @@ package im.angry.openeuicc.core -import im.angry.openeuicc.util.* import net.typeblog.lpac_jni.LocalProfileAssistant -class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { +class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel by orig { private var _inner: EuiccChannel? = orig private val channel: EuiccChannel @@ -15,28 +14,11 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { return _inner!! } - override val type: String - get() = channel.type - override val port: UiccPortInfoCompat - get() = channel.port - override val slotId: Int - get() = channel.slotId - override val logicalSlotId: Int - get() = channel.logicalSlotId - override val portId: Int - get() = channel.portId private val lpaDelegate = lazy { LocalProfileAssistantWrapper(channel.lpa) } - override val lpa: LocalProfileAssistant by lpaDelegate - override val valid: Boolean - get() = channel.valid - override val intrinsicChannelName: String? - get() = channel.intrinsicChannelName - override val atr: ByteArray? - get() = channel.atr - override fun close() = channel.close() + override val lpa: LocalProfileAssistant by lpaDelegate fun invalidateWrapper() { _inner = null 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..8d06a28 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,9 +7,9 @@ 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 +import java.util.concurrent.atomic.AtomicInteger class OmapiApduInterface( private val service: SEService, @@ -21,7 +21,8 @@ class OmapiApduInterface( } private lateinit var session: Session - private lateinit var lastChannel: Channel + private val channels = mutableMapOf() + private val index = AtomicInteger(0) override val valid: Boolean get() = service.isConnected && (this::session.isInitialized && !session.isClosed) @@ -38,23 +39,25 @@ 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)!! + val id = index.addAndGet(1) + channels[id] = channel + return id } override fun logicalChannelClose(handle: Int) { - check(handle == 1 && !this::lastChannel.isInitialized) { - "Unknown channel" + val channel = channels[handle] + check(channel != null) { + "Invalid logical channel handle $handle" } - lastChannel.close() + channels.remove(handle) + channel.close() } - override fun transmit(tx: ByteArray): ByteArray { - check(this::lastChannel.isInitialized) { - "Unknown channel" + override fun transmit(handle: Int, tx: ByteArray): ByteArray { + val channel = channels[handle] + check(channel != null) { + "Invalid logical channel handle $handle" } if (runBlocking { verboseLoggingFlow.first() }) { @@ -63,7 +66,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-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt index 5123d53..2630743 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt @@ -20,12 +20,12 @@ data class UsbCcidDescription( private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000 private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000 - private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000 + private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000 // bVoltageSupport Masks - private const val VOLTAGE_5V: Byte = 1 - private const val VOLTAGE_3V: Byte = 2 - private const val VOLTAGE_1_8V: Byte = 4 + private const val VOLTAGE_5V0: Byte = 1 + private const val VOLTAGE_3V0: Byte = 2 + private const val VOLTAGE_1V8: Byte = 4 private const val SLOT_OFFSET = 4 private const val FEATURES_OFFSET = 40 @@ -71,10 +71,12 @@ data class UsbCcidDescription( } enum class Voltage(powerOnValue: Int, mask: Int) { - AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V( - 3, - VOLTAGE_1_8V.toInt() - ); + // @formatter:off + AUTO(0, 0), + V50(1, VOLTAGE_5V0.toInt()), + V30(2, VOLTAGE_3V0.toInt()), + V18(3, VOLTAGE_1V8.toInt()); + // @formatter:on val mask = powerOnValue.toByte() val powerOnValue = mask.toByte() @@ -84,18 +86,14 @@ data class UsbCcidDescription( (dwFeatures and feature) != 0 val voltages: Array - get() = + get() { if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) { - arrayOf(Voltage.AUTO) - } else { - Voltage.values().mapNotNull { - if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) { - it - } else { - null - } - }.toTypedArray() + return arrayOf(Voltage.AUTO) } + return Voltage.entries + .mapNotNull { if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) it else null } + .toTypedArray() + } val hasAutomaticPps: Boolean get() = hasFeature(FEATURE_AUTOMATIC_PPS) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt index 5ef35af..7b6cd15 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt @@ -95,6 +95,7 @@ class UsbCcidTransceiver( data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) : Exception(msg) + @Suppress("ArrayInDataClass") data class CcidDataBlock( val dwLength: Int, val bSlot: Byte, @@ -183,31 +184,26 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (runBlocking { verboseLoggingFlow.first() }) { - Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex()) + 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] - ) + 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)) + } + }) } var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer) if (expectedSequenceNumber != result.bSeq) { - throw UsbTransportException( - ("USB-CCID error - expected sequence number " + - expectedSequenceNumber + ", got " + result) - ) + throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result") } val dataBuffer = ByteArray(result.dwLength) @@ -218,9 +214,7 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (readBytes < 0) { - throw UsbTransportException( - "USB error - failed reading response data! Header: $result" - ) + throw UsbTransportException("USB error - failed reading response data! Header: $result") } System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes) bufferedBytes += readBytes @@ -285,7 +279,7 @@ class UsbCcidTransceiver( } val ccidDataBlock = receiveDataBlock(sequenceNumber) val elapsedTime = SystemClock.elapsedRealtime() - startTime - Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms") + Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms") return ccidDataBlock } @@ -314,8 +308,11 @@ class UsbCcidTransceiver( val elapsedTime = SystemClock.elapsedRealtime() - startTime Log.d( TAG, - "Usb transport connected, took " + elapsedTime + "ms, ATR=" + - response.data?.encodeHex() + buildString { + append("Usb transport connected") + append(", took ", elapsedTime, "ms") + append(", ATR=", response.data?.encodeHex()) + } ) return response } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt index edca7a0..877c7fd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt @@ -6,31 +6,22 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbInterface -class UsbTransportException(msg: String) : Exception(msg) +class UsbTransportException(message: String) : Exception(message) -fun UsbInterface.getIoEndpoints(): Pair { - var bulkIn: UsbEndpoint? = null - var bulkOut: UsbEndpoint? = null - for (i in 0 until endpointCount) { - val endpoint = getEndpoint(i) - if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { - continue - } - if (endpoint.direction == UsbConstants.USB_DIR_IN) { - bulkIn = endpoint - } else if (endpoint.direction == UsbConstants.USB_DIR_OUT) { - bulkOut = endpoint - } - } - return Pair(bulkIn, bulkOut) -} +val UsbDevice.interfaces: Iterable + get() = (0 until interfaceCount).map(::getInterface) -fun UsbDevice.getSmartCardInterface(): UsbInterface? { - for (i in 0 until interfaceCount) { - val anInterface = getInterface(i) - if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) { - return anInterface - } +val Iterable.smartCard: UsbInterface? + get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID } + +val UsbInterface.endpoints: Iterable + get() = (0 until endpointCount).map(::getEndpoint) + +val Iterable.bulkPair: Pair + get() { + val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK } + return Pair( + endpoints.find { it.direction == UsbConstants.USB_DIR_IN }, + endpoints.find { it.direction == UsbConstants.USB_DIR_OUT }, + ) } - return null -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index e88ad01..528b232 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -23,6 +23,8 @@ import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* +import im.angry.openeuicc.vendored.getESTKmeInfo +import im.angry.openeuicc.vendored.getSIMLinkVersion import kotlinx.coroutines.launch import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI @@ -100,24 +102,22 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { add(Item(R.string.euicc_info_access_mode, channel.type)) - add( - Item( - R.string.euicc_info_removable, - formatByBoolean(channel.port.card.isRemovable, YES_NO) - ) - ) - add( - Item( - R.string.euicc_info_eid, - channel.lpa.eID, - copiedToastResId = R.string.toast_eid_copied - ) - ) + add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) + add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) + getESTKmeInfo(channel.apduInterface)?.let { + add(Item(R.string.euicc_info_sku, it.skuName)) + add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied)) + add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion)) + add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion)) + } + getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let { + add(Item(R.string.euicc_info_sku, "9eSIM $it")) + } channel.lpa.euiccInfo2.let { info -> - add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version)) - add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) - add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) - add(Item(R.string.euicc_info_pp_version, info?.ppVersion)) + add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString())) + add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString())) + add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString())) + add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString())) add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) } @@ -134,13 +134,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - add( - Item( - R.string.euicc_info_atr, - channel.atr?.encodeHex() ?: getString(R.string.information_unavailable), - copiedToastResId = R.string.toast_atr_copied, - ) - ) + val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) + add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } private fun formatByBoolean(b: Boolean, res: Pair): String = diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index 8d72462..f252d76 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -12,13 +12,10 @@ fun String.decodeHex(): ByteArray { return out } -fun ByteArray.encodeHex(): String { - val sb = StringBuilder() - val length = size - for (i in 0 until length) { - sb.append(String.format("%02X", this[i])) +fun ByteArray.encodeHex(): String = buildString { + for (element in this@encodeHex) { + append(String.format("%02X", element)) } - return sb.toString() } fun formatFreeSpace(size: Int): String = diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt new file mode 100644 index 0000000..c3ed3b5 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt @@ -0,0 +1,36 @@ +package im.angry.openeuicc.vendored + +import android.util.Log +import im.angry.openeuicc.util.TAG +import im.angry.openeuicc.util.decodeHex +import net.typeblog.lpac_jni.ApduInterface + +data class ESTKmeInfo( + val serialNumber: String?, + val bootloaderVersion: String?, + val firmwareVersion: String?, + val skuName: String?, +) + +fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? { + fun decode(b: ByteArray): String? { + if (b.size < 2) return null + if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null + return b.sliceArray(0 until b.size - 2).decodeToString() + } + return try { + iface.openChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { + fun invoke(p1: Byte) = decode(it.transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00))) + ESTKmeInfo( + invoke(0x00), // serial number + invoke(0x01), // bootloader version + invoke(0x02), // firmware version + invoke(0x03), // sku name + ) + } + } catch (e: Exception) { + Log.d(TAG, "Failed to get ESTKmeInfo", e) + null + } +} + diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt new file mode 100644 index 0000000..89641ac --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt @@ -0,0 +1,19 @@ +package im.angry.openeuicc.vendored + +import net.typeblog.lpac_jni.Version + +private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix + +fun getSIMLinkVersion(eid: String, version: Version?): String? { + if (version == null || prefix.find(eid, 0) == null) return null + return when { + // @formatter:off + version >= Version(36, 18, 5) -> "v3 (final)" + version >= Version(36, 17, 39) -> "v3 (beta)" + version >= Version(36, 17, 4) -> "v2s" + version >= Version(36, 9, 3) -> "v2.1" + version >= Version(36, 7, 2) -> "v2" + // @formatter:on + else -> null + } +} diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 71e2418..a45ce1f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Cannot switch to new eSIM profile. Confirmation string mismatch ICCID copied to clipboard + Serial Number copied to clipboard EID copied to clipboard ATR copied to clipboard @@ -125,6 +126,10 @@ eUICC Info (%s) Access Mode Removable + Product Name + Product Serial Number + Product Bootloader Version + Product Firmware Version EID SGP.22 Version eUICC OS Version 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..ced7ff1 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,19 @@ interface ApduInterface { * callers should further check with the LPA to fully determine the validity of a channel */ val valid: Boolean + + fun openChannel(aid: ByteArray, callback: (TransmitProvider) -> T): T { + val handle = logicalChannelOpen(aid) + return try { + callback(object : TransmitProvider { + override fun transmit(tx: ByteArray) = transmit(handle, tx) + }) + } finally { + logicalChannelClose(handle) + } + } +} + +interface TransmitProvider { + fun transmit(tx: ByteArray): ByteArray } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt index 6c73051..0720049 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt @@ -2,14 +2,31 @@ package net.typeblog.lpac_jni /* Corresponds to EuiccInfo2 in SGP.22 */ data class EuiccInfo2( - val sgp22Version: String, - val profileVersion: String, - val euiccFirmwareVersion: String, - val globalPlatformVersion: String, + val sgp22Version: Version, + val profileVersion: Version, + val euiccFirmwareVersion: Version, + val globalPlatformVersion: Version, val sasAccreditationNumber: String, - val ppVersion: String, + val ppVersion: Version, val freeNvram: Int, val freeRam: Int, - val euiccCiPKIdListForSigning: Array, - val euiccCiPKIdListForVerification: Array, -) \ No newline at end of file + val euiccCiPKIdListForSigning: Set, + val euiccCiPKIdListForVerification: Set, +) + +data class Version( + val major: Int, + val minor: Int, + val patch: Int, +) { + constructor(version: String) : this(version.split('.').map(String::toInt)) + private constructor(parts: List) : this(parts[0], parts[1], parts[2]) + + operator fun compareTo(other: Version): Int { + if (major != other.major) return major - other.major + if (minor != other.minor) return minor - other.minor + return patch - other.patch + } + + override fun toString() = "$major.$minor.$patch" +} 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..12b5d7e 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 @@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback +import net.typeblog.lpac_jni.Version class LocalProfileAssistantImpl( isdrAid: ByteArray, @@ -28,9 +29,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 } @@ -84,8 +85,8 @@ class LocalProfileAssistantImpl( throw IllegalArgumentException("Failed to initialize LPA") } - val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf() - httpInterface.usePublicKeyIds(pkids) + val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf() + httpInterface.usePublicKeyIds(pkids.toTypedArray()) } override fun setEs10xMss(mss: Byte) { @@ -172,16 +173,16 @@ class LocalProfileAssistantImpl( } val ret = EuiccInfo2( - LpacJni.euiccInfo2GetSGP22Version(cInfo), - LpacJni.euiccInfo2GetProfileVersion(cInfo), - LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo), - LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo), + Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)), + Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)), + Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)), + Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)), LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo), - LpacJni.euiccInfo2GetPpVersion(cInfo), + Version(LpacJni.euiccInfo2GetPpVersion(cInfo)), LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), - euiccCiPKIdListForSigning.toTypedArray(), - euiccCiPKIdListForVerification.toTypedArray() + euiccCiPKIdListForSigning.toTypedArray().toSet(), + euiccCiPKIdListForVerification.toTypedArray().toSet(), ) LpacJni.euiccInfo2Free(cInfo) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt index cfd5779..295a911 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt @@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795 // List of GSMA Live CIs // https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/ -val PKID_GSMA_LIVE_CI = arrayOf( +val PKID_GSMA_LIVE_CI = setOf( // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert) // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt DEFAULT_PKID_GSMA_RSP2_ROOT_CI1, @@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = arrayOf( // SGP.26 v3.0, 2023-12-01 // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf -val PKID_GSMA_TEST_CI = arrayOf( +val PKID_GSMA_TEST_CI = setOf( // Test CI (SGP.26, NIST P256) // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt "34eecf13156518d48d30bdf06853404d115f955d", 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..b51f6a8 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", @@ -66,11 +66,14 @@ static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t 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 = ctx->apdu._internal.logic_channel; 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)); -- 2.45.3 From 30279aeac7182280edb39f22fb7ddabc78be5f5f Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 15:40:50 +0800 Subject: [PATCH 02/13] fix .gitignore --- .idea/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/.gitignore b/.idea/.gitignore index 0d51aca..b7c2402 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -9,5 +9,6 @@ /navEditor.xml /runConfigurations.xml /workspace.xml +/AndroidProjectSystem.xml **/*.iml \ No newline at end of file -- 2.45.3 From f12f6ccaa2478e06d827f7c594b61603b2a2f932 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 16:53:46 +0800 Subject: [PATCH 03/13] checking omapi logical channel open failed --- .../java/im/angry/openeuicc/core/OmapiApduInterface.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 8d06a28..bbf8fdd 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 @@ -39,7 +39,8 @@ class OmapiApduInterface( } override fun logicalChannelOpen(aid: ByteArray): Int { - val channel = session.openLogicalChannel(aid)!! + val channel = session.openLogicalChannel(aid) + check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } val id = index.addAndGet(1) channels[id] = channel return id @@ -47,11 +48,9 @@ class OmapiApduInterface( override fun logicalChannelClose(handle: Int) { val channel = channels[handle] - check(channel != null) { - "Invalid logical channel handle $handle" - } + check(channel != null) { "Invalid logical channel handle $handle" } channels.remove(handle) - channel.close() + if (channel.isOpen) channel.close() } override fun transmit(handle: Int, tx: ByteArray): ByteArray { -- 2.45.3 From 3913e2f5d47240296c114669df7936158bc51e01 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 17:31:48 +0800 Subject: [PATCH 04/13] detect is estkme card --- .../src/main/java/im/angry/openeuicc/vendored/estkme.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt index c3ed3b5..86132be 100644 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt @@ -1,8 +1,10 @@ package im.angry.openeuicc.vendored import android.util.Log +import im.angry.openeuicc.core.ApduInterfaceAtrProvider import im.angry.openeuicc.util.TAG import im.angry.openeuicc.util.decodeHex +import im.angry.openeuicc.util.encodeHex import net.typeblog.lpac_jni.ApduInterface data class ESTKmeInfo( @@ -12,7 +14,11 @@ data class ESTKmeInfo( val skuName: String?, ) +fun isESTKmeATR(atr: ByteArray?): Boolean = + atr != null && atr.encodeHex().contains("estk.me".encodeToByteArray().encodeHex()) + fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? { + if (!isESTKmeATR((iface as ApduInterfaceAtrProvider?)?.atr)) return null fun decode(b: ByteArray): String? { if (b.size < 2) return null if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null -- 2.45.3 From fd13867615887e2faf19c38d5d809d8434e13dc8 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 18:17:45 +0800 Subject: [PATCH 05/13] detect is estkme card --- .../java/im/angry/openeuicc/vendored/estkme.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt index 86132be..b6efab1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt @@ -4,7 +4,6 @@ import android.util.Log import im.angry.openeuicc.core.ApduInterfaceAtrProvider import im.angry.openeuicc.util.TAG import im.angry.openeuicc.util.decodeHex -import im.angry.openeuicc.util.encodeHex import net.typeblog.lpac_jni.ApduInterface data class ESTKmeInfo( @@ -14,11 +13,19 @@ data class ESTKmeInfo( val skuName: String?, ) -fun isESTKmeATR(atr: ByteArray?): Boolean = - atr != null && atr.encodeHex().contains("estk.me".encodeToByteArray().encodeHex()) +fun isESTKmeATR(iface: ApduInterface): Boolean { + if (iface !is ApduInterfaceAtrProvider) return false + val atr = iface.atr ?: return false + val fpr = "estk.me".encodeToByteArray() + for (index in atr.indices) { + if (atr.size - index < fpr.size) break + if (atr.sliceArray(index until index + fpr.size).contentEquals(fpr)) return true + } + return false +} fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? { - if (!isESTKmeATR((iface as ApduInterfaceAtrProvider?)?.atr)) return null + if (!isESTKmeATR(iface)) return null fun decode(b: ByteArray): String? { if (b.size < 2) return null if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null -- 2.45.3 From c2117093440c883ac1b7d75a05fce5d48d109a51 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 19:52:08 +0800 Subject: [PATCH 06/13] revert: EuiccChannelWrapper --- .../openeuicc/core/EuiccChannelWrapper.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) 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 1a108e5..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,8 +1,10 @@ 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 by orig { +class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { private var _inner: EuiccChannel? = orig private val channel: EuiccChannel @@ -14,11 +16,30 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel by orig { return _inner!! } + override val type: String + get() = channel.type + override val port: UiccPortInfoCompat + get() = channel.port + override val slotId: Int + get() = channel.slotId + override val logicalSlotId: Int + get() = channel.logicalSlotId + override val portId: Int + get() = channel.portId private val lpaDelegate = lazy { LocalProfileAssistantWrapper(channel.lpa) } - override val lpa: LocalProfileAssistant by lpaDelegate + override val valid: Boolean + get() = channel.valid + override val intrinsicChannelName: String? + get() = channel.intrinsicChannelName + override val apduInterface: ApduInterface + get() = channel.apduInterface + override val atr: ByteArray? + get() = channel.atr + + override fun close() = channel.close() fun invalidateWrapper() { _inner = null -- 2.45.3 From 1f9a9d5f22c378bc73c09090dee6d38f2e455068 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 20:26:08 +0800 Subject: [PATCH 07/13] improve usb driver voltage handling --- .../im/angry/openeuicc/core/usb/UsbCcidDescription.kt | 10 +++------- .../im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt | 8 ++++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt index 2630743..93ef4c7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt @@ -85,14 +85,10 @@ data class UsbCcidDescription( private fun hasFeature(feature: Int): Boolean = (dwFeatures and feature) != 0 - val voltages: Array + val voltages: List get() { - if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) { - return arrayOf(Voltage.AUTO) - } - return Voltage.entries - .mapNotNull { if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) it else null } - .toTypedArray() + if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO) + return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 } } val hasAutomaticPps: Boolean diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt index 7b6cd15..9155721 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt @@ -287,13 +287,13 @@ class UsbCcidTransceiver( 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") + for (voltage in usbCcidDescription.voltages) { + Log.v(TAG, "CCID: attempting to power on with voltage $voltage") response = try { - iccPowerOnVoltage(v.powerOnValue) + iccPowerOnVoltage(voltage.powerOnValue) } catch (e: UsbCcidErrorException) { if (e.errorResponse.bError.toInt() == 7) { // Power select error - Log.v(TAG, "CCID: failed to power on with voltage $v") + Log.v(TAG, "CCID: failed to power on with voltage $voltage") iccPowerOff() Log.v(TAG, "CCID: powered off") continue -- 2.45.3 From ba8a4b87cba619d8d71a9087278f5fa04bc05f73 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 20:35:01 +0800 Subject: [PATCH 08/13] revert: partial code --- .../core/DefaultEuiccChannelFactory.kt | 5 +- .../core/DefaultEuiccChannelManager.kt | 5 +- .../im/angry/openeuicc/core/EuiccChannel.kt | 7 --- .../angry/openeuicc/core/EuiccChannelImpl.kt | 5 +- .../openeuicc/core/EuiccChannelWrapper.kt | 3 -- .../openeuicc/core/usb/UsbCcidDescription.kt | 36 ++++++++------ .../openeuicc/core/usb/UsbCcidTransceiver.kt | 49 ++++++++++--------- .../angry/openeuicc/core/usb/UsbCcidUtils.kt | 43 +++++++++------- 8 files changed, 79 insertions(+), 74 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index ea0bd60..5e87564 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -8,8 +8,7 @@ import android.se.omapi.SEService import android.util.Log import im.angry.openeuicc.common.R 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.getIoEndpoints import im.angry.openeuicc.util.* import java.lang.IllegalArgumentException @@ -62,7 +61,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha } override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? { - val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair + val (bulkIn, bulkOut) = usbInterface.getIoEndpoints() if (bulkIn == null || bulkOut == null) return null val conn = usbManager.openDevice(usbDevice) ?: return null if (!conn.claimInterface(usbInterface, true)) return null diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index dd57eab..293042c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -5,8 +5,7 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.telephony.SubscriptionManager import android.util.Log -import im.angry.openeuicc.core.usb.smartCard -import im.angry.openeuicc.core.usb.interfaces +import im.angry.openeuicc.core.usb.getSmartCardInterface import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -245,7 +244,7 @@ open class DefaultEuiccChannelManager( withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") - val iface = device.interfaces.smartCard ?: return@forEach + val iface = device.getSmartCardInterface() ?: return@forEach // If we don't have permission, tell UI code that we found a candidate device, but we // need permission to be able to do anything with it if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) 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 0ecca21..5f399ea 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,8 +1,6 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* -import im.angry.openeuicc.vendored.ESTKmeInfo -import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant interface EuiccChannel { @@ -30,10 +28,5 @@ 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 a56b1cc..3da829a 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,7 +1,6 @@ package im.angry.openeuicc.core -import im.angry.openeuicc.util.UiccPortInfoCompat -import im.angry.openeuicc.util.decodeHex +import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.Flow import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant @@ -12,7 +11,7 @@ class EuiccChannelImpl( override val type: String, override val port: UiccPortInfoCompat, override val intrinsicChannelName: String?, - override val apduInterface: ApduInterface, + private 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 09004d3..4204e82 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,7 +1,6 @@ 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 { @@ -34,8 +33,6 @@ 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/usb/UsbCcidDescription.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt index 93ef4c7..5123d53 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt @@ -20,12 +20,12 @@ data class UsbCcidDescription( private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000 private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000 - private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000 + private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000 // bVoltageSupport Masks - private const val VOLTAGE_5V0: Byte = 1 - private const val VOLTAGE_3V0: Byte = 2 - private const val VOLTAGE_1V8: Byte = 4 + private const val VOLTAGE_5V: Byte = 1 + private const val VOLTAGE_3V: Byte = 2 + private const val VOLTAGE_1_8V: Byte = 4 private const val SLOT_OFFSET = 4 private const val FEATURES_OFFSET = 40 @@ -71,12 +71,10 @@ data class UsbCcidDescription( } enum class Voltage(powerOnValue: Int, mask: Int) { - // @formatter:off - AUTO(0, 0), - V50(1, VOLTAGE_5V0.toInt()), - V30(2, VOLTAGE_3V0.toInt()), - V18(3, VOLTAGE_1V8.toInt()); - // @formatter:on + AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V( + 3, + VOLTAGE_1_8V.toInt() + ); val mask = powerOnValue.toByte() val powerOnValue = mask.toByte() @@ -85,11 +83,19 @@ data class UsbCcidDescription( private fun hasFeature(feature: Int): Boolean = (dwFeatures and feature) != 0 - val voltages: List - get() { - if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO) - return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 } - } + val voltages: Array + get() = + if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) { + arrayOf(Voltage.AUTO) + } else { + Voltage.values().mapNotNull { + if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) { + it + } else { + null + } + }.toTypedArray() + } val hasAutomaticPps: Boolean get() = hasFeature(FEATURE_AUTOMATIC_PPS) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt index 9155721..5ef35af 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt @@ -95,7 +95,6 @@ class UsbCcidTransceiver( data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) : Exception(msg) - @Suppress("ArrayInDataClass") data class CcidDataBlock( val dwLength: Int, val bSlot: Byte, @@ -184,26 +183,31 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (runBlocking { verboseLoggingFlow.first() }) { - Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}") + 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()) { - 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)) - } - }) + 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") + throw UsbTransportException( + ("USB-CCID error - expected sequence number " + + expectedSequenceNumber + ", got " + result) + ) } val dataBuffer = ByteArray(result.dwLength) @@ -214,7 +218,9 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (readBytes < 0) { - throw UsbTransportException("USB error - failed reading response data! Header: $result") + throw UsbTransportException( + "USB error - failed reading response data! Header: $result" + ) } System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes) bufferedBytes += readBytes @@ -279,7 +285,7 @@ class UsbCcidTransceiver( } val ccidDataBlock = receiveDataBlock(sequenceNumber) val elapsedTime = SystemClock.elapsedRealtime() - startTime - Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms") + Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms") return ccidDataBlock } @@ -287,13 +293,13 @@ class UsbCcidTransceiver( val startTime = SystemClock.elapsedRealtime() skipAvailableInput() var response: CcidDataBlock? = null - for (voltage in usbCcidDescription.voltages) { - Log.v(TAG, "CCID: attempting to power on with voltage $voltage") + for (v in usbCcidDescription.voltages) { + Log.v(TAG, "CCID: attempting to power on with voltage $v") response = try { - iccPowerOnVoltage(voltage.powerOnValue) + 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 $voltage") + Log.v(TAG, "CCID: failed to power on with voltage $v") iccPowerOff() Log.v(TAG, "CCID: powered off") continue @@ -308,11 +314,8 @@ class UsbCcidTransceiver( val elapsedTime = SystemClock.elapsedRealtime() - startTime Log.d( TAG, - buildString { - append("Usb transport connected") - append(", took ", elapsedTime, "ms") - append(", ATR=", response.data?.encodeHex()) - } + "Usb transport connected, took " + elapsedTime + "ms, ATR=" + + response.data?.encodeHex() ) return response } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt index 877c7fd..edca7a0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt @@ -6,22 +6,31 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbInterface -class UsbTransportException(message: String) : Exception(message) +class UsbTransportException(msg: String) : Exception(msg) -val UsbDevice.interfaces: Iterable - get() = (0 until interfaceCount).map(::getInterface) - -val Iterable.smartCard: UsbInterface? - get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID } - -val UsbInterface.endpoints: Iterable - get() = (0 until endpointCount).map(::getEndpoint) - -val Iterable.bulkPair: Pair - get() { - val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK } - return Pair( - endpoints.find { it.direction == UsbConstants.USB_DIR_IN }, - endpoints.find { it.direction == UsbConstants.USB_DIR_OUT }, - ) +fun UsbInterface.getIoEndpoints(): Pair { + var bulkIn: UsbEndpoint? = null + var bulkOut: UsbEndpoint? = null + for (i in 0 until endpointCount) { + val endpoint = getEndpoint(i) + if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { + continue + } + if (endpoint.direction == UsbConstants.USB_DIR_IN) { + bulkIn = endpoint + } else if (endpoint.direction == UsbConstants.USB_DIR_OUT) { + bulkOut = endpoint + } } + return Pair(bulkIn, bulkOut) +} + +fun UsbDevice.getSmartCardInterface(): UsbInterface? { + for (i in 0 until interfaceCount) { + val anInterface = getInterface(i) + if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) { + return anInterface + } + } + return null +} \ No newline at end of file -- 2.45.3 From fd4023194a1e387cfe4f76ae19f4adff6753de4a Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 20:35:57 +0800 Subject: [PATCH 09/13] revert: partial code --- .../src/main/java/im/angry/openeuicc/util/StringUtils.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index f252d76..8d72462 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -12,10 +12,13 @@ fun String.decodeHex(): ByteArray { return out } -fun ByteArray.encodeHex(): String = buildString { - for (element in this@encodeHex) { - append(String.format("%02X", element)) +fun ByteArray.encodeHex(): String { + val sb = StringBuilder() + val length = size + for (i in 0 until length) { + sb.append(String.format("%02X", this[i])) } + return sb.toString() } fun formatFreeSpace(size: Int): String = -- 2.45.3 From 13092ee37a61da4c65dd4b5c17bb65007d28f2d9 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 21:49:31 +0800 Subject: [PATCH 10/13] revert: partial code --- .../src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt | 1 - 1 file changed, 1 deletion(-) 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 a6a5cf0..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 -- 2.45.3 From 2f186054707c195ede2d14b95a0497d91cc0f5c1 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 21:50:22 +0800 Subject: [PATCH 11/13] revert: partial code --- .../src/main/java/net/typeblog/lpac_jni/ApduInterface.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3689363..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 @@ -17,10 +17,10 @@ interface ApduInterface { */ val valid: Boolean - fun withLogicalChannel(aid: ByteArray, callback: ((ByteArray) -> ByteArray) -> T): T { + fun withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T { val handle = logicalChannelOpen(aid) return try { - callback { tx -> transmit(handle, tx) } + cb { transmit(handle, it) } } finally { logicalChannelClose(handle) } -- 2.45.3 From ea2970c1b54d69c6002a88c59c203927ee3a7be9 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 26 Feb 2025 22:11:01 +0800 Subject: [PATCH 12/13] refactor: euicc info 2 --- .../impl/LocalProfileAssistantImpl.kt | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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 12b5d7e..3674f4f 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 @@ -158,20 +158,6 @@ class LocalProfileAssistantImpl( val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) if (cInfo == 0L) return null - val euiccCiPKIdListForSigning = mutableListOf() - var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) - while (curr != 0L) { - euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr)) - curr = LpacJni.stringArrNext(curr) - } - - val euiccCiPKIdListForVerification = mutableListOf() - curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo) - while (curr != 0L) { - euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr)) - curr = LpacJni.stringArrNext(curr) - } - val ret = EuiccInfo2( Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)), Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)), @@ -181,8 +167,20 @@ class LocalProfileAssistantImpl( Version(LpacJni.euiccInfo2GetPpVersion(cInfo)), LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), - euiccCiPKIdListForSigning.toTypedArray().toSet(), - euiccCiPKIdListForVerification.toTypedArray().toSet(), + buildSet { + var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) + while (cursor != 0L) { + add(LpacJni.stringDeref(cursor)) + cursor = LpacJni.stringArrNext(cursor) + } + }, + buildSet { + var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo) + while (cursor != 0L) { + add(LpacJni.stringDeref(cursor)) + cursor = LpacJni.stringArrNext(cursor) + } + }, ) LpacJni.euiccInfo2Free(cInfo) -- 2.45.3 From b4a622c932ff89330c888119b3cc58bfde995183 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 2 Mar 2025 19:10:35 +0800 Subject: [PATCH 13/13] chore: add 9esim v3.1 (beta 1) supports --- app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt index 89641ac..506f16c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt @@ -8,6 +8,7 @@ fun getSIMLinkVersion(eid: String, version: Version?): String? { if (version == null || prefix.find(eid, 0) == null) return null return when { // @formatter:off + version >= Version(37, 1, 41) -> "v3.1 (beta 1)" version >= Version(36, 18, 5) -> "v3 (final)" version >= Version(36, 17, 39) -> "v3 (beta)" version >= Version(36, 17, 4) -> "v2s" -- 2.45.3