Compare commits

...

7 commits

Author SHA1 Message Date
eaef00b88a Fixup README 2025-05-18 11:17:43 -04:00
023f6ded28 Update README.md to add a comparison matrix for Easy/OpenEUICC 2025-05-04 21:10:57 -04:00
a601ab7d72 fix: send euicc memory reset notification (#189)
```console
$ ./lpac profile list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "iccid": "8944476500001320600",
        "isdpAid": "a0000005591010ffffffff8900001100",
        "profileState": "enabled",
        "profileNickname": null,
        "serviceProviderName": "BetterRoaming",
        "profileName": "BetterRoaming",
        "iconType": null,
        "icon": null,
        "profileClass": "operational"
      },
      {
        "iccid": "89861234567891232113",
        "isdpAid": "a0000005591010ffffffff8900001200",
        "profileState": "disabled",
        "profileNickname": null,
        "serviceProviderName": "rspmanager_tester2",
        "profileName": "20230625_yysx",
        "iconType": null,
        "icon": null,
        "profileClass": "operational"
      }
    ]
  }
}
$ ./lpac notification list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "seqNumber": 48,
        "profileManagementOperation": "install",
        "notificationAddress": "smdp.io",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 49,
        "profileManagementOperation": "enable",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 50,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 51,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      }
    ]
  }
}
$ ./lpac chip purge yes
{"type":"lpa","payload":{"code":0,"message":"success","data":null}}
$ ./lpac notification list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "seqNumber": 48,
        "profileManagementOperation": "install",
        "notificationAddress": "smdp.io",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 49,
        "profileManagementOperation": "enable",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 50,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 51,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 52,
        "profileManagementOperation": "delete",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 53,
        "profileManagementOperation": "delete",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      }
    ]
  }
}
```

Reviewed-on: PeterCxy/OpenEUICC#189
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-14 03:36:38 +02:00
756c621d5e fix: stricted sm-dp+ address checking (#190)
Reviewed-on: PeterCxy/OpenEUICC#190
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-14 03:36:14 +02:00
68114fa863 Expose the current ISD-R AID in use 2025-04-05 20:50:10 -04:00
1fda120459 Avoid reconnecting to USB iface repeatedly while trying different AIDs 2025-04-05 20:44:15 -04:00
994324acb6 Fix up back button for IsdrAidListActivity 2025-04-05 17:58:56 -04:00
14 changed files with 172 additions and 78 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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}"

View file

@ -34,5 +34,10 @@ interface EuiccChannel {
*/
val apduInterface: ApduInterface
/**
* The AID of the ISD-R channel currently in use
*/
val isdrAid: ByteArray
fun close()
}

View file

@ -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?

View file

@ -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<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean>
) : EuiccChannel {

View file

@ -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()

View file

@ -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<Boolean>
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<Int>()
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

View file

@ -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<Boolean>
) {
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
}
}
}

View file

@ -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()
}
}
}

View file

@ -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)) }

View file

@ -62,6 +62,11 @@ class IsdrAidListActivity : AppCompatActivity() {
true
}
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}

View file

@ -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
}

View file

@ -134,6 +134,7 @@
<string name="euicc_info_bl_ver">Product Bootloader Version</string>
<string name="euicc_info_fw_ver">Product Firmware Version</string>
<string name="euicc_info_eid" translatable="false">EID</string>
<string name="euicc_info_isdr_aid" translatable="false">ISD-R AID</string>
<string name="euicc_info_sgp22_version">SGP.22 Version</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>