diff --git a/README.md b/README.md index f953f9e..f8019b2 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,18 @@ A fully free and open-source Local Profile Assistant implementation for Android devices. -There are two variants of this project, OpenEUICC and EasyEUICC: +There are two variants of this project: -| | 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`. +- 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` __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 0de99b5..870baae 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,17 +1,25 @@ 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.UsbCcidContext +import im.angry.openeuicc.core.usb.bulkPair +import im.angry.openeuicc.core.usb.endpoints 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) @@ -64,16 +72,24 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha } override fun tryOpenUsbEuiccChannel( - ccidCtx: UsbCcidContext, + usbDevice: UsbDevice, + usbInterface: UsbInterface, 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 = ccidCtx.productName, + intrinsicChannelName = usbDevice.productName, UsbApduInterface( - ccidCtx + conn, + bulkIn, + bulkOut, + context.preferenceRepository.verboseLoggingFlow ), 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 6b336cd..ac9ba08 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,6 @@ 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 @@ -276,15 +275,11 @@ 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(ccidCtx, it) + euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface, it) } if (channel != null && channel.lpa.valid) { - ccidCtx.allowDisconnect = true usbChannel = channel return@withContext Pair(device, true) } @@ -292,10 +287,6 @@ 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 b20932f..597a70d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -34,10 +34,5 @@ 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 ba587a6..87f5885 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,6 +1,7 @@ package im.angry.openeuicc.core -import im.angry.openeuicc.core.usb.UsbCcidContext +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbInterface import im.angry.openeuicc.util.* // This class is here instead of inside DI because it contains a bit more logic than just @@ -9,7 +10,8 @@ interface EuiccChannelFactory { suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel? fun tryOpenUsbEuiccChannel( - ccidCtx: UsbCcidContext, + usbDevice: UsbDevice, + usbInterface: UsbInterface, 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 2a33c20..ed8797a 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, - override val isdrAid: ByteArray, + 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 361a943..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 @@ -38,8 +38,6 @@ 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 4a4ccb9..107395f 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,19 +1,27 @@ 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 ccidCtx: UsbCcidContext + private val conn: UsbDeviceConnection, + private val bulkIn: UsbEndpoint, + private val bulkOut: UsbEndpoint, + private val verboseLoggingFlow: Flow ) : ApduInterface, ApduInterfaceAtrProvider { companion object { private const val TAG = "UsbApduInterface" } - override val atr: ByteArray? - get() = ccidCtx.atr + private lateinit var ccidDescription: UsbCcidDescription + private lateinit var transceiver: UsbCcidTransceiver + + override var atr: ByteArray? = null override val valid: Boolean get() = channels.isNotEmpty() @@ -21,7 +29,22 @@ class UsbApduInterface( private var channels = mutableSetOf() override fun connect() { - ccidCtx.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 + } // Send Terminal Capabilities // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY @@ -33,7 +56,11 @@ class UsbApduInterface( transmitApduByChannel(terminalCapabilities, 0) } - override fun disconnect() = ccidCtx.disconnect() + override fun disconnect() { + conn.close() + + atr = null + } override fun logicalChannelOpen(aid: ByteArray): Int { // OPEN LOGICAL CHANNEL @@ -122,7 +149,7 @@ class UsbApduInterface( // OR the channel mask into the CLA byte realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte() - var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!! + var resp = transceiver.sendXfrBlock(realTx).data!! if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!") @@ -133,7 +160,7 @@ class UsbApduInterface( // 0x6C = wrong le // so we fix the le field here realTx[realTx.size - 1] = resp[resp.size - 1] - resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!! + resp = transceiver.sendXfrBlock(realTx).data!! } else if (sw1 == 0x61) { // 0x61 = X bytes available // continue reading by GET RESPONSE @@ -143,7 +170,7 @@ class UsbApduInterface( realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte() ) - val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!! + val tmp = 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 deleted file mode 100644 index caf69e7..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt +++ /dev/null @@ -1,87 +0,0 @@ -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 4744321..2a24488 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,6 +12,7 @@ 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 @@ -516,12 +517,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_euicc_memory_reset_failure), R.drawable.ic_euicc_memory_reset ) { - euiccChannelManager.beginTrackedOperation(slotId, portId) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.euiccMemoryReset() - } - - preferenceRepository.notificationDeleteFlow.first() + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.euiccMemoryReset() } } } \ 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 1d5f37f..aa922be 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,7 +102,6 @@ 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 022a391..6553421 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,11 +62,6 @@ 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 1c69de5..402e7a5 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,9 +1,11 @@ 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 @@ -84,34 +86,10 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF } private fun updateInputCompleteness() { - inputComplete = isValidAddress(smdp.editText!!.text) + inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches() 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 05ca15a..5e4e4da 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -134,7 +134,6 @@ Product Bootloader Version Product Firmware Version EID - ISD-R AID SGP.22 Version eUICC OS Version GlobalPlatform Version