diff --git a/README.md b/README.md index f8019b2..f953f9e 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,22 @@ A fully free and open-source Local Profile Assistant implementation for Android devices. -There are two variants of this project: +There are two variants of this project, OpenEUICC and EasyEUICC: -- OpenEUICC: The full-fledged privileged variant. - - Due to its privilege requirement, OpenEUICC must be placed inside `/system/priv-app` and be signed with the platform certificate. - - The preferred way to including OpenEUICC in a system image is to [build it along with AOSP](#building-aosp). - - __Note__: When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant. -- EasyEUICC: Unprivileged version that can run as a user app. - - This version supports two modes of operation: - 1. Inserted, removable eSIMs: Due to obvious security requirements, EasyEUICC is only able to access eSIM chips whose [ARF/ARA](https://source.android.com/docs/core/connect/uicc#arf) contains the hash of EasyEUICC's signing certificate. - 2. USB CCID Card Readers: Only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported. In this mode, EasyEUICC can access any eSIM chip loaded in the card reader regardless of their ARF/ARA, as long as they implement the [SGP.22 standard](https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf). - - Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases) - - For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA` +| | OpenEUICC | EasyEUICC | +|:------------------------------|:-----------------------------------------------:|:-----------------:| +| Privileged | Must be installed as system app | No | +| Internal eSIM | Supported | Unsupported | +| External (Removable) eSIM | Supported | Supported | +| USB Readers | Supported | Supported | +| Requires allowlisting by eSIM | No | Yes -- except USB | +| System Integration | Partial (carrier partner API unimplemented yet) | No | + +Some side notes: +1. When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant. +2. Both variants support accessing eUICC chips through USB CCID readers, regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. However, only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported. +3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs can be found in the CI page. +4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`. __This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request. 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 870baae..0de99b5 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 @@ -1,25 +1,17 @@ package im.angry.openeuicc.core import android.content.Context -import android.hardware.usb.UsbDevice -import android.hardware.usb.UsbInterface -import android.hardware.usb.UsbManager import android.se.omapi.SEService import android.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.UsbCcidContext import im.angry.openeuicc.util.* import java.lang.IllegalArgumentException open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory { private var seService: SEService? = null - private val usbManager by lazy { - context.getSystemService(Context.USB_SERVICE) as UsbManager - } - private suspend fun ensureSEService() { if (seService == null || !seService!!.isConnected) { seService = connectSEService(context) @@ -72,24 +64,16 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha } override fun tryOpenUsbEuiccChannel( - usbDevice: UsbDevice, - usbInterface: UsbInterface, + ccidCtx: UsbCcidContext, isdrAid: ByteArray ): EuiccChannel? { - val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair - if (bulkIn == null || bulkOut == null) return null - val conn = usbManager.openDevice(usbDevice) ?: return null - if (!conn.claimInterface(usbInterface, true)) return null try { return EuiccChannelImpl( context.getString(R.string.usb), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), - intrinsicChannelName = usbDevice.productName, + intrinsicChannelName = ccidCtx.productName, UsbApduInterface( - conn, - bulkIn, - bulkOut, - context.preferenceRepository.verboseLoggingFlow + ccidCtx ), isdrAid, context.preferenceRepository.verboseLoggingFlow, 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 ac9ba08..6b336cd 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,6 +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.UsbCcidContext import im.angry.openeuicc.core.usb.smartCard import im.angry.openeuicc.core.usb.interfaces import im.angry.openeuicc.di.AppContainer @@ -275,11 +276,15 @@ open class DefaultEuiccChannelManager( TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel" ) + + val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach + try { val channel = tryOpenChannelFirstValidAid { - euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface, it) + euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it) } if (channel != null && channel.lpa.valid) { + ccidCtx.allowDisconnect = true usbChannel = channel return@withContext Pair(device, true) } @@ -287,6 +292,10 @@ open class DefaultEuiccChannelManager( // Ignored -- skip forward e.printStackTrace() } + + ccidCtx.allowDisconnect = true + ccidCtx.disconnect() + Log.i( TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}" 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..b20932f 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 @@ -34,5 +34,10 @@ interface EuiccChannel { */ val apduInterface: ApduInterface + /** + * The AID of the ISD-R channel currently in use + */ + val isdrAid: ByteArray + fun close() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt index 87f5885..ba587a6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt @@ -1,7 +1,6 @@ package im.angry.openeuicc.core -import android.hardware.usb.UsbDevice -import android.hardware.usb.UsbInterface +import im.angry.openeuicc.core.usb.UsbCcidContext import im.angry.openeuicc.util.* // This class is here instead of inside DI because it contains a bit more logic than just @@ -10,8 +9,7 @@ interface EuiccChannelFactory { suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel? fun tryOpenUsbEuiccChannel( - usbDevice: UsbDevice, - usbInterface: UsbInterface, + ccidCtx: UsbCcidContext, isdrAid: ByteArray ): EuiccChannel? 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 ed8797a..2a33c20 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 @@ -13,7 +13,7 @@ class EuiccChannelImpl( override val port: UiccPortInfoCompat, override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, - isdrAid: ByteArray, + override val isdrAid: ByteArray, 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..361a943 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 @@ -38,6 +38,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { get() = channel.apduInterface override val atr: ByteArray? get() = channel.atr + override val isdrAid: ByteArray + get() = channel.isdrAid override fun close() = channel.close() 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 107395f..4a4ccb9 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 @@ -1,27 +1,19 @@ package im.angry.openeuicc.core.usb -import android.hardware.usb.UsbDeviceConnection -import android.hardware.usb.UsbEndpoint import android.util.Log import im.angry.openeuicc.core.ApduInterfaceAtrProvider import im.angry.openeuicc.util.* -import kotlinx.coroutines.flow.Flow import net.typeblog.lpac_jni.ApduInterface class UsbApduInterface( - private val conn: UsbDeviceConnection, - private val bulkIn: UsbEndpoint, - private val bulkOut: UsbEndpoint, - private val verboseLoggingFlow: Flow + private val ccidCtx: UsbCcidContext ) : ApduInterface, ApduInterfaceAtrProvider { companion object { private const val TAG = "UsbApduInterface" } - private lateinit var ccidDescription: UsbCcidDescription - private lateinit var transceiver: UsbCcidTransceiver - - override var atr: ByteArray? = null + override val atr: ByteArray? + get() = ccidCtx.atr override val valid: Boolean get() = channels.isNotEmpty() @@ -29,22 +21,7 @@ class UsbApduInterface( private var channels = mutableSetOf() override fun connect() { - ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!! - - if (!ccidDescription.hasT0Protocol) { - throw IllegalArgumentException("Unsupported card reader; T=0 support is required") - } - - transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow) - - try { - // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40) - // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf - atr = transceiver.iccPowerOn().data - } catch (e: Exception) { - e.printStackTrace() - throw e - } + ccidCtx.connect() // Send Terminal Capabilities // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY @@ -56,11 +33,7 @@ class UsbApduInterface( transmitApduByChannel(terminalCapabilities, 0) } - override fun disconnect() { - conn.close() - - atr = null - } + override fun disconnect() = ccidCtx.disconnect() override fun logicalChannelOpen(aid: ByteArray): Int { // OPEN LOGICAL CHANNEL @@ -149,7 +122,7 @@ class UsbApduInterface( // OR the channel mask into the CLA byte realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte() - var resp = transceiver.sendXfrBlock(realTx).data!! + var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!! if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!") @@ -160,7 +133,7 @@ class UsbApduInterface( // 0x6C = wrong le // so we fix the le field here realTx[realTx.size - 1] = resp[resp.size - 1] - resp = transceiver.sendXfrBlock(realTx).data!! + resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!! } else if (sw1 == 0x61) { // 0x61 = X bytes available // continue reading by GET RESPONSE @@ -170,7 +143,7 @@ class UsbApduInterface( realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte() ) - val tmp = transceiver.sendXfrBlock(getResponseCmd).data!! + val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!! resp = resp.sliceArray(0 until (resp.size - 2)) + tmp diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt new file mode 100644 index 0000000..caf69e7 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt @@ -0,0 +1,87 @@ +package im.angry.openeuicc.core.usb + +import android.content.Context +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbDeviceConnection +import android.hardware.usb.UsbEndpoint +import android.hardware.usb.UsbInterface +import android.hardware.usb.UsbManager +import im.angry.openeuicc.util.preferenceRepository +import kotlinx.coroutines.flow.Flow + +/** + * A wrapper over an usb device + interface, manages the lifecycle independent + * of the APDU interface exposed to lpac-jni. + * + * This allows us to try multiple AIDs on each interface without opening / closing + * the USB connection numerous times. + */ +class UsbCcidContext private constructor( + private val conn: UsbDeviceConnection, + private val bulkIn: UsbEndpoint, + private val bulkOut: UsbEndpoint, + val productName: String, + val verboseLoggingFlow: Flow +) { + companion object { + fun createFromUsbDevice( + context: Context, + usbDevice: UsbDevice, + usbInterface: UsbInterface + ): UsbCcidContext? = runCatching { + val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair + if (bulkIn == null || bulkOut == null) return@runCatching null + val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice) + ?: return@runCatching null + if (!conn.claimInterface(usbInterface, true)) return@runCatching null + UsbCcidContext( + conn, + bulkIn, + bulkOut, + usbDevice.productName ?: "USB", + context.preferenceRepository.verboseLoggingFlow + ) + }.getOrNull() + } + + /** + * When set to false (the default), the disconnect() method does nothing. + * This allows the separation of device disconnection from lpac-jni's APDU interface. + */ + var allowDisconnect = false + private var initialized = false + lateinit var transceiver: UsbCcidTransceiver + var atr: ByteArray? = null + + fun connect() { + if (initialized) { + return + } + + val ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!! + + if (!ccidDescription.hasT0Protocol) { + throw IllegalArgumentException("Unsupported card reader; T=0 support is required") + } + + transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow) + + try { + // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40) + // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf + atr = transceiver.iccPowerOn().data + } catch (e: Exception) { + e.printStackTrace() + throw e + } + + initialized = true + } + + fun disconnect() { + if (initialized && allowDisconnect) { + conn.close() + atr = null + } + } +} diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 2a24488..4744321 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -12,7 +12,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R -import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -517,8 +516,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_euicc_memory_reset_failure), R.drawable.ic_euicc_memory_reset ) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.euiccMemoryReset() + euiccChannelManager.beginTrackedOperation(slotId, portId) { + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.euiccMemoryReset() + } + + preferenceRepository.notificationDeleteFlow.first() } } } \ 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 aa922be..1d5f37f 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 @@ -102,6 +102,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { 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_isdr_aid, channel.isdrAid.encodeHex())) channel.tryParseEuiccVendorInfo()?.let { vendorInfo -> vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) } vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt index 6553421..022a391 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt @@ -62,6 +62,11 @@ class IsdrAidListActivity : AppCompatActivity() { true } + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index 402e7a5..1c69de5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -1,11 +1,9 @@ package im.angry.openeuicc.ui.wizard import android.os.Bundle -import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.widget.addTextChangedListener import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R @@ -86,10 +84,34 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF } private fun updateInputCompleteness() { - inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches() + inputComplete = isValidAddress(smdp.editText!!.text) if (state.confirmationCodeRequired) { inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty() } refreshButtons() } +} + +private fun isValidAddress(input: CharSequence): Boolean { + if (!input.contains('.')) return false + var fqdn = input + var port = 443 + if (input.contains(':')) { + val portIndex = input.lastIndexOf(':') + fqdn = input.substring(0, portIndex) + port = input.substring(portIndex + 1, input.length).toIntOrNull(10) ?: 0 + } + // see https://en.wikipedia.org/wiki/Port_(computer_networking) + if (port < 1 || port > 0xffff) return false + // see https://en.wikipedia.org/wiki/Fully_qualified_domain_name + if (fqdn.isEmpty() || fqdn.length > 255) return false + for (part in fqdn.split('.')) { + if (part.isEmpty() || part.length > 64) return false + if (part.first() == '-' || part.last() == '-') return false + for (c in part) { + if (c.isLetterOrDigit() || c == '-') continue + return false + } + } + return true } \ 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 5e4e4da..05ca15a 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -134,6 +134,7 @@ Product Bootloader Version Product Firmware Version EID + ISD-R AID SGP.22 Version eUICC OS Version GlobalPlatform Version