diff --git a/.idea/.gitignore b/.idea/.gitignore index b7c2402..0d51aca 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -9,6 +9,5 @@ /navEditor.xml /runConfigurations.xml /workspace.xml -/AndroidProjectSystem.xml **/*.iml \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 8096d6c..46b2f38 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -2,10 +2,10 @@ - + - + diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index e805548..fe63bb6 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file 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 597a70d..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,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 interface EuiccChannel { @@ -29,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..a82cb97 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,25 +11,16 @@ 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 { - companion object { - // TODO: This needs to go somewhere else. - val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex() - } - override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex override val portId = port.portIndex override val lpa: LocalProfileAssistant = - LocalProfileAssistantImpl( - ISDR_AID, - apduInterface, - HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow) - ) + LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)) override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr 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/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index b3f42b5..c70669d 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,6 +7,7 @@ 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 @@ -20,12 +21,7 @@ class OmapiApduInterface( } private lateinit var session: Session - private val channels = arrayOf( - null, - null, - null, - null, - ) + private lateinit var lastChannel: Channel override val valid: Boolean get() = service.isConnected && (this::session.isInitialized && !session.isClosed) @@ -42,24 +38,24 @@ class OmapiApduInterface( } override fun logicalChannelOpen(aid: ByteArray): Int { - 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 + check(!this::lastChannel.isInitialized) { + "Can only open one channel" + } + lastChannel = session.openLogicalChannel(aid)!! + return 1 } override fun logicalChannelClose(handle: Int) { - val channel = channels.getOrNull(handle) - check(channel != null) { "Invalid logical channel handle $handle" } - if (channel.isOpen) channel.close() - synchronized(channels) { channels[handle] = null } + check(handle == 1 && !this::lastChannel.isInitialized) { + "Unknown channel" + } + lastChannel.close() } - override fun transmit(handle: Int, tx: ByteArray): ByteArray { - val channel = channels.getOrNull(handle) - check(channel != null) { "Invalid logical channel handle $handle" } + override fun transmit(tx: ByteArray): ByteArray { + check(this::lastChannel.isInitialized) { + "Unknown channel" + } if (runBlocking { verboseLoggingFlow.first() }) { Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}") @@ -67,7 +63,7 @@ class OmapiApduInterface( try { for (i in 0..10) { - val res = channel.transmit(tx) + val res = lastChannel.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 f9e764b..624ef89 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,13 +21,10 @@ 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)!! @@ -49,11 +46,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) @@ -69,7 +66,7 @@ class UsbApduInterface( return -1 } - val channelId = resp[0].toInt() + channelId = resp[0].toInt() Log.d(TAG, "channelId = $channelId") // Then, select AID @@ -81,32 +78,32 @@ class UsbApduInterface( return -1 } - channels.add(channelId) - return channelId } override fun logicalChannelClose(handle: Int) { - check(channels.contains(handle)) { - "Invalid logical channel handle $handle" - } + check(handle == channelId) { "Logical channel ID mismatch" } + check(channelId != -1) { "Logical channel is not opened" } + // CLOSE LOGICAL CHANNEL - val req = manageChannelCmd(false, handle.toByte()) - val resp = transmitApduByChannel(req, handle.toByte()) + val req = manageChannelCmd(false, channelId.toByte()) + val resp = transmitApduByChannel(req, channelId.toByte()) if (!isSuccessResponse(resp)) { Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}") } - channels.remove(handle) + + channelId = -1 } - override fun transmit(handle: Int, tx: ByteArray): ByteArray { - check(channels.contains(handle)) { - "Invalid logical channel handle $handle" - } - return transmitApduByChannel(tx, handle.toByte()) + override fun transmit(tx: ByteArray): ByteArray { + check(channelId != -1) { "Logical channel is not opened" } + return transmitApduByChannel(tx, channelId.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 bc32fb6..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,24 +71,31 @@ 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() } - private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0 + 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 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 528b232..e88ad01 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,8 +23,6 @@ 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 @@ -102,22 +100,24 @@ 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)) - 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")) - } + 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 + ) + ) channel.lpa.euiccInfo2.let { info -> - 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_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_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) } @@ -134,8 +134,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) - add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) + add( + Item( + R.string.euicc_info_atr, + channel.atr?.encodeHex() ?: getString(R.string.information_unavailable), + 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/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 2846fd7..6203364 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -124,22 +124,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard processLpaString(text.toString()) } - private fun processLpaString(input: String) { - try { - val parsed = ActivationCode.fromString(input) - state.smdp = parsed.address - state.matchingId = parsed.matchingId - if (parsed.confirmationCodeRequired) { - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.profile_download_required_confirmation_code) - setMessage(R.string.profile_download_required_confirmation_code_message) - setCancelable(true) - setPositiveButton(android.R.string.ok, null) - show() - } - } - gotoNextFragment(DownloadWizardDetailsFragment()) - } catch (e: IllegalArgumentException) { + private fun processLpaString(s: String) { + val components = s.split("$") + if (components.size < 3 || components[0] != "LPA:1") { AlertDialog.Builder(requireContext()).apply { setTitle(R.string.profile_download_incorrect_lpa_string) setMessage(R.string.profile_download_incorrect_lpa_string_message) @@ -147,7 +134,11 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard setNegativeButton(android.R.string.cancel, null) show() } + return } + state.smdp = components[1] + state.matchingId = components[2] + gotoNextFragment(DownloadWizardDetailsFragment()) } private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt deleted file mode 100644 index 3aca0d6..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt +++ /dev/null @@ -1,23 +0,0 @@ -package im.angry.openeuicc.util - -data class ActivationCode( - val address: String, - val matchingId: String? = null, - val oid: String? = null, - val confirmationCodeRequired: Boolean = false, -) { - companion object { - fun fromString(input: String): ActivationCode { - val components = input.removePrefix("LPA:").split('$') - if (components.size < 2 || components[0] != "1") { - throw IllegalArgumentException("Invalid activation code format") - } - return ActivationCode( - address = components[1].trim(), - matchingId = components.getOrNull(2)?.trim()?.ifBlank { null }, - oid = components.getOrNull(3)?.trim()?.ifBlank { null }, - confirmationCodeRequired = components.getOrNull(4)?.trim() == "1" - ) - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 2282921..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt +++ /dev/null @@ -1,49 +0,0 @@ -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 net.typeblog.lpac_jni.ApduInterface - -data class ESTKmeInfo( - val serialNumber: String?, - val bootloaderVersion: String?, - val firmwareVersion: String?, - val skuName: String?, -) - -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)) 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 - return b.sliceArray(0 until b.size - 2).decodeToString() - } - return try { - iface.withLogicalChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { transmit -> - fun invoke(p1: Byte) = decode(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 deleted file mode 100644 index 506f16c..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt +++ /dev/null @@ -1,20 +0,0 @@ -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(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" - 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-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index 1710a7d..b592ec3 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -144,6 +144,4 @@ テスティング 準備中 動作中 - 要確認コード - スキャンされた QR コード、又はクリップボードからペーストされた LPA コードには、確認コードの必要が表示されています。 diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml index 2cadc03..cf51734 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -144,6 +144,4 @@ 无视 SM-DP+ 的 TLS 证书 允许 RSP 服务器使用任意证书 无信息 - 需要确认码 - 您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码 \ No newline at end of file diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml deleted file mode 100644 index 28691a0..0000000 --- a/app-common/src/main/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。 - 此 eSIM 上還沒有設定檔 - 未知 - 幫助 - 重新載入卡槽 - 虛擬卡槽 %d - 已啟用 - 已停用 - 電信業者: - 類型: - 啟用 - 停用 - 刪除 - 重新命名 - 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 - 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 - 無法切換到新的 eSIM 設定檔。 - 輸入的確認文字不匹配 - 已複製 ICCID 到剪貼簿 - 已複製 EID 到剪貼簿 - 已複製 ATR 到剪貼簿 - 授予 USB 權限 - 需要獲得訪問 USB 晶片讀卡機的權限。 - 無法透過 USB 晶片讀卡機連線到 eSIM。 - 長時間運行的背景作業 - 正在下載 eSIM 設定檔 - 無法下載 eSIM 設定檔 - 正在重新命名 eSIM 設定檔 - 無法重新命名 eSIM 設定檔 - 正在刪除 eSIM 設定檔 - 無法刪除 eSIM 設定檔 - 正在切換 eSIM 設定檔 - 無法切換 eSIM 設定檔 - 新增新 eSIM - 伺服器 (RSP / SM-DP+) - 啟用碼 - 確認碼 (可選) - IMEI (可選) - 本次下載可能會失敗 - 目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載? - 日誌已儲存到指定路徑。需要透過其他 App 分享嗎? - 新名稱 - 無法將名稱編碼為 UTF-8 - 名稱長於 64 字元 - 重新命名設定檔時發生了未知錯誤 - 您確定要刪除 %s 嗎?此動作無法還原。 - 請輸入\'%s\'以確認刪除 - 通知列表 - 通知列表 (%s) - 管理通知 - eSIM 設定檔可以在下載、刪除、啟用或停用時向電信業者傳送通知。此處列出了要傳送的這些通知的佇列。\n\n在\"設定\"中,您可以指定是否自動傳送每種型別的通知。請注意,即使通知已傳送,也不會自動從記錄中刪除,除非佇列空間不足。\n\n在這裡,您可以手動傳送或刪除每個待處理的通知。 - 已下載 - 已刪除 - 已啟用 - 已停用 - 處理 - 刪除 - 儲存日誌 - %s 的日誌 - 設定 - 通知 - 變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。 - 下載 - 傳送 下載 設定檔的通知 - 刪除 - 傳送 刪除 設定檔的通知 - 切換 - 記錄詳細日誌 - 詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。 - 日誌 - 檢視應用程式的最新除錯日誌 - 傳送 切換 設定檔的通知\n注意,這種型別的通知是不可靠的。 - 進階 - 允許 停用/刪除 已啟用的設定檔 - 預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。 - 資訊 - App 版本 - 原始碼 - 測試 - 準備中 - 可用 - 未在剪貼簿上發現 LPA 碼 - LPA 碼解析錯誤 - 無法將二維碼或剪貼簿內容解析為 LPA 碼 - 下載精靈 - 返回 - 下一步 - 您選擇的 SIM 卡已被移除 - 請選擇或確認下載目標 eSIM 卡槽: - 型別: - 可插拔 - 內建 - 內建, 埠 %d - 目前設定檔: - 剩餘空間: - 您想要如何下載 eSIM 設定檔? - 用相機掃描二維碼 - 從相簿選擇二維碼 - 從剪貼簿讀取 - 手動輸入 - 請輸入或確認下載 eSIM 的詳細資訊: - 正在下載您的 eSIM... - 準備中 - 正在連線到伺服器 - 正在向伺服器驗證您的裝置 - 正在下載 eSIM 設定檔 - 正在寫入 eSIM 設定檔 - 錯誤診斷 - 錯誤代碼: %s - 上次 HTTP 狀態碼 (來自伺服器): %d - 上次 HTTP 應答 (來自伺服器): - 上次 HTTP 錯誤: - 上次 APDU 應答 (來自 SIM): %s - 上次 APDU 應答 (來自 SIM) 是成功的 - 上次 APDU 應答 (來自 SIM) 是失敗的 - 上次 APDU 錯誤: - 儲存 - %s 的錯誤診斷 - eUICC 詳情 - eUICC 詳情 (%s) - 訪問方式 - 可插拔 - SGP.22 版本 - eUICC OS 版本 - GlobalPlatform 版本 - SAS 認證號碼 - Protected Profile 版本 - NVRAM 剩餘空間 (eSIM 儲存容量) - 證書簽發者 (CI) - GSMA 生產環境 CI - GSMA 測試 CI - 未知 eSIM CI - - - 還有 %d 步成為開發者 - 您現在是開發者了! - 語言 - 選擇 App 語言 - 開發人員選項 - 顯示未經過濾的設定檔列表 - 在設定檔列表中包括非生產環境的設定檔 - 忽略 SM-DP+ 的 TLS 證書 - 允許 RSP 伺服器使用任意證書 - 無資訊 - \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index a45ce1f..d3bce00 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -31,7 +31,6 @@ 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 @@ -58,8 +57,6 @@ This download may fail This download may fail due to low remaining capacity. No LPA code found in clipboard - Confirmation Code Required - Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard. Unable to parse Could not parse QR code or clipboard content as a LPA code. @@ -126,10 +123,6 @@ 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-unpriv/src/main/res/values-zh-rTW/strings.xml b/app-unpriv/src/main/res/values-zh-rTW/strings.xml deleted file mode 100644 index b8d0eb8..0000000 --- a/app-unpriv/src/main/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - 相容性檢查 - 啟動 SIM 卡應用程式 - 系統功能 - 您的裝置是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的電話功能和 OMAPI 支援。 - 您的裝置沒有電話功能。 - 您的裝置/系統未宣告支援 OMAPI。這可能是由於缺少硬體支援,或者可能僅僅是由於缺少標誌。請參閱以下兩項檢查以確定 OMAPI 是否確實受支援。 - OMAPI 連線 - 您的裝置是否允許透過 OMAPI 存取 SIM 卡上的安全元件? - 無法透過 OMAPI 偵測到 SIM 卡的 Secure Element。如果您尚未在此裝置中插入 SIM 卡,請嘗試插入一張 SIM 卡並重試此檢查。 - 已成功檢測到可存取 Secure Element 的卡槽,但僅限於以下 SIM 卡槽:SIM%s - ISD-R 通道存取 - 您的裝置是否支援透過 OMAPI 開啟 eSIM 的 ISD-R (管理) 通道? - 無法確定是否支援透過 OMAPI 進行 ISD-R 的存取。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重試。 - OMAPI 只能在以下 SIM 插槽上存取 ISD-R:SIM%s - 不在已知錯誤清單中 - 確保您的裝置不存在與可插拔 eSIM 相關的錯誤。 - 很抱歉,您的裝置在存取可插拔 eSIM 時存在錯誤。這並不表示完全無法使用,但我們不保證該應用在您裝置上的行為。 - USB 晶片讀卡機支援 - 您的裝置是否支援透過 USB 晶片讀卡機管理 eSIM? - 您可以透過此裝置上的標準 USB CCID 讀卡機管理 eSIM (即使您在這裡有任何其他檢查項失敗)。請插入讀卡機,然後開啟此應用程式以這種方式管理 eSIM。 - 您的裝置不支援 USB 晶片讀卡機。 - 結論 (USB 晶片讀卡機以外) - 根據之前的所有檢查,您的裝置與可插拔 eSIM 卡相容的可能性有多大? - 您可以使用和管理插入此裝置的可插拔 eSIM 卡。 - 已知您的裝置在存取可插拔 eSIM 卡時存在問題。\n%s - 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。不過,您的裝置確實宣告支援 OMAPI,因此它工作的可能性略高。\n%s - 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。由於您的裝置未宣告支援OMAPI,因此更有可能不支援在此裝置上管理可插拔 eSIM。\n%s - 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。\n%s - 然而,已經載入了eSIM設定檔的可插拔 eSIM 卡仍然可以工作; 即使無法在裝置上直接管理可插拔 eSIM 卡中的設定檔,您仍然可以使用 USB 卡讀卡機來管理設定檔。 - ARA-M SHA-1 已複製到剪貼簿 - \ No newline at end of file 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 f0b1909..6b09368 100644 --- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt +++ b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt @@ -18,10 +18,12 @@ class TelephonyManagerApduInterface( const val TAG = "TelephonyManagerApduInterface" } - override val valid: Boolean - get() = channels.isNotEmpty() + private var lastChannel: Int = -1 - private var channels = mutableSetOf() + override val valid: Boolean + // TelephonyManager channels will never become truly "invalid", + // just that transactions might return errors or nonsense + get() = lastChannel != -1 override fun connect() { // Do nothing @@ -29,39 +31,52 @@ 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 TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}") + throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}") } - channels.add(channel.channel) - return channel.channel + lastChannel = channel.channel + return lastChannel } override fun logicalChannelClose(handle: Int) { - check(channels.contains(handle)) { - "Invalid logical channel handle $handle" - } + check(handle == lastChannel) { "Invalid channel handle " } tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle) - channels.remove(handle) + lastChannel = -1 } - override fun transmit(handle: Int, tx: ByteArray): ByteArray { - check(channels.contains(handle)) { - "Invalid logical channel handle $handle" - } + override fun transmit(tx: ByteArray): ByteArray { + check(lastChannel != -1) { "Uninitialized" } + if (runBlocking { verboseLoggingFlow.first() }) { Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}") } - 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() + + 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() } + } \ 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 a9df18e..dbd39f2 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt @@ -111,26 +111,15 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat( } fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat( - 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) { + 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) { iccTransmitApduLogicalChannelByPort( - slotIndex, portIndex, channel, - cla, ins, p1, p2, p3, p4 + slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data ) } else { iccTransmitApduLogicalChannelBySlot( - slotIndex, channel, - cla, ins, p1, p2, p3, p4 + slotIndex, channel, cla, inst, p1, p2, p3, data ) } -} diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml deleted file mode 100644 index 368efbc..0000000 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - 在此裝置上找不到 eUICC 晶片。\n在某些裝置上,您可能需要先在此應用的選單中啟用雙卡支援。 - 雙卡 - 雙卡支援狀態已切換。請等待基頻處理器重新啟動。 - 此卡槽支援多個啟用設定檔 (MEP)。要啟用或停用此功能,請使用\"卡槽對映\"工具。 - 卡槽對映 - 虛擬卡槽 %d: - 卡槽 %1$d 端口 %2$d - 您的手機有 %1$d 個虛擬 SIM 卡槽和 %2$d 個實體 SIM 卡槽。%3$s\n\n選擇您希望每個虛擬卡槽對應的實體卡槽 和/或 \"端口\"。請注意,並非所有對映模式都受硬體支援。 - \n\n實體卡槽 %1$d 支援多個啟用的設定檔 (MEP)。要使用此功能,請將其 %2$d 個虛擬\"端口\"分配給上面顯示的不同虛擬卡槽。\n\n啟用 MEP 後,\"端口\"會在 OpenEUICC 中顯示為共享 eSIM 設定檔的獨立的 eSIM 卡槽。 - \n支援雙卡模式,但已停用。如果您的裝置帶有內建 eSIM 晶片,則預設情況下可能不會啟用。更改上面的對映或啟用雙卡以訪問您的 eSIM。 - 您的新卡槽對映已設定完畢。請等待基頻處理器重新整理卡槽。 - 指定的對映可能無效或硬體不支援您指定的對映。 - 透過下載 eSIM 連線到行動網路 - 您的裝置支援 eSIM。要連線到行動網路,請下載電信業者釋出的 eSIM,或插入實體 SIM 卡。 - 跳過 - 下載 eSIM - TelephonyManager (特權) - \ No newline at end of file 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 75a6905..dfa92df 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(handle: Int, tx: ByteArray): ByteArray + fun transmit(tx: ByteArray): ByteArray /** * Is this APDU connection still valid? @@ -16,13 +16,4 @@ interface ApduInterface { * callers should further check with the LPA to fully determine the validity of a channel */ val valid: Boolean - - fun withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T { - val handle = logicalChannelOpen(aid) - return try { - cb { transmit(handle, it) } - } finally { - logicalChannelClose(handle) - } - } -} +} \ 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 0720049..6c73051 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,31 +2,14 @@ package net.typeblog.lpac_jni /* Corresponds to EuiccInfo2 in SGP.22 */ data class EuiccInfo2( - val sgp22Version: Version, - val profileVersion: Version, - val euiccFirmwareVersion: Version, - val globalPlatformVersion: Version, + val sgp22Version: String, + val profileVersion: String, + val euiccFirmwareVersion: String, + val globalPlatformVersion: String, val sasAccreditationNumber: String, - val ppVersion: Version, + val ppVersion: String, val freeNvram: Int, val freeRam: Int, - 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" -} + val euiccCiPKIdListForSigning: Array, + val euiccCiPKIdListForVerification: Array, +) \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index fa9474f..370fcab 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -5,11 +5,7 @@ internal object LpacJni { System.loadLibrary("lpac-jni") } - external fun createContext( - isdrAid: ByteArray, - apduInterface: ApduInterface, - httpInterface: HttpInterface - ): Long + external fun createContext(apduInterface: ApduInterface, httpInterface: HttpInterface): Long external fun destroyContext(handle: Long) external fun euiccInit(handle: Long): Int 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 3674f4f..7310acd 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,10 +10,8 @@ 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, rawApduInterface: ApduInterface, rawHttpInterface: HttpInterface ): LocalProfileAssistant { @@ -29,9 +27,9 @@ class LocalProfileAssistantImpl( var lastApduResponse: ByteArray? = null var lastApduException: Exception? = null - override fun transmit(handle: Int, tx: ByteArray): ByteArray = + override fun transmit(tx: ByteArray): ByteArray = try { - apduInterface.transmit(handle, tx).also { + apduInterface.transmit(tx).also { lastApduException = null lastApduResponse = it } @@ -78,15 +76,15 @@ class LocalProfileAssistantImpl( private val httpInterface = HttpInterfaceWrapper(rawHttpInterface) private var finalized = false - private var contextHandle: Long = LpacJni.createContext(isdrAid, apduInterface, httpInterface) + private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) init { if (LpacJni.euiccInit(contextHandle) < 0) { throw IllegalArgumentException("Failed to initialize LPA") } - val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf() - httpInterface.usePublicKeyIds(pkids.toTypedArray()) + val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf() + httpInterface.usePublicKeyIds(pkids) } override fun setEs10xMss(mss: Byte) { @@ -158,29 +156,31 @@ 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)), - Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)), - Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)), + LpacJni.euiccInfo2GetSGP22Version(cInfo), + LpacJni.euiccInfo2GetProfileVersion(cInfo), + LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo), + LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo), LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo), - Version(LpacJni.euiccInfo2GetPpVersion(cInfo)), + LpacJni.euiccInfo2GetPpVersion(cInfo), LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), - 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) - } - }, + euiccCiPKIdListForSigning.toTypedArray(), + euiccCiPKIdListForVerification.toTypedArray() ) 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 295a911..cfd5779 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 = setOf( +val PKID_GSMA_LIVE_CI = arrayOf( // 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 = setOf( // 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 = setOf( +val PKID_GSMA_TEST_CI = arrayOf( // 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 b/libs/lpac-jni/src/main/jni/lpac index 90f7104..a5a0516 160000 --- a/libs/lpac-jni/src/main/jni/lpac +++ b/libs/lpac-jni/src/main/jni/lpac @@ -1 +1 @@ -Subproject commit 90f7104847d4bb392b275746da20a55177a67573 +Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef 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 a61fc96..9059171 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", "(I[B)[B"); + method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B"); jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface"); method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit", @@ -53,30 +53,24 @@ 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, - __attribute__((unused)) uint8_t channel) { +static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, 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, logical_channel_id); + method_apdu_logical_channel_close, channel); (*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, logic_channel, txArr - ); + jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface, + method_apdu_transmit, 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 ca319db..38d4f3a 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 jchar _unused[1]; + const char _unused[1]; empty_string = (*env)->NewString(env, _unused, 0); empty_string = (*env)->NewGlobalRef(env, empty_string); @@ -37,30 +37,17 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_createContext(JNIEnv *env, jobject thiz, - jbyteArray isdr_aid, jobject apdu_interface, jobject http_interface) { struct lpac_jni_ctx *jni_ctx = NULL; struct euicc_ctx *ctx = NULL; - jbyte *isdr_java = NULL; - uint32_t isdr_len = 0; - uint8_t *isdr_c = NULL; ctx = calloc(1, sizeof(struct euicc_ctx)); jni_ctx = calloc(1, sizeof(struct lpac_jni_ctx)); - - isdr_java = (*env)->GetByteArrayElements(env, isdr_aid, JNI_FALSE); - isdr_len = (*env)->GetArrayLength(env, isdr_aid); - isdr_c = calloc(isdr_len, sizeof(uint8_t)); - memcpy(isdr_c, isdr_java, isdr_len); - (*env)->ReleaseByteArrayElements(env, isdr_aid, isdr_java, JNI_ABORT); - ctx->apdu.interface = &lpac_jni_apdu_interface; ctx->http.interface = &lpac_jni_http_interface; jni_ctx->apdu_interface = (*env)->NewGlobalRef(env, apdu_interface); jni_ctx->http_interface = (*env)->NewGlobalRef(env, http_interface); - ctx->aid = (const uint8_t *) isdr_c; - ctx->aid_len = isdr_len; ctx->userdata = (void *) jni_ctx; return (jlong) ctx; } @@ -73,7 +60,6 @@ Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jl (*env)->DeleteGlobalRef(env, jni_ctx->apdu_interface); (*env)->DeleteGlobalRef(env, jni_ctx->http_interface); free(jni_ctx); - free((void *) ctx->aid); free(ctx); } 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 c2300be..a5d6262 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,7 +8,6 @@ _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; };