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. 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. | | OpenEUICC | EasyEUICC |
- 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). | Privileged | Must be installed as system app | No |
- __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. | Internal eSIM | Supported | Unsupported |
- EasyEUICC: Unprivileged version that can run as a user app. | External (Removable) eSIM | Supported | Supported |
- This version supports two modes of operation: | USB Readers | Supported | Supported |
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. | Requires allowlisting by eSIM | No | Yes -- except USB |
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). | System Integration | Partial (carrier partner API unimplemented yet) | No |
- 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` 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. __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 package im.angry.openeuicc.core
import android.content.Context 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.se.omapi.SEService
import android.util.Log import android.util.Log
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.usb.UsbApduInterface import im.angry.openeuicc.core.usb.UsbApduInterface
import im.angry.openeuicc.core.usb.bulkPair import im.angry.openeuicc.core.usb.UsbCcidContext
import im.angry.openeuicc.core.usb.endpoints
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory { open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
private var seService: SEService? = null private var seService: SEService? = null
private val usbManager by lazy {
context.getSystemService(Context.USB_SERVICE) as UsbManager
}
private suspend fun ensureSEService() { private suspend fun ensureSEService() {
if (seService == null || !seService!!.isConnected) { if (seService == null || !seService!!.isConnected) {
seService = connectSEService(context) seService = connectSEService(context)
@ -72,24 +64,16 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
} }
override fun tryOpenUsbEuiccChannel( override fun tryOpenUsbEuiccChannel(
usbDevice: UsbDevice, ccidCtx: UsbCcidContext,
usbInterface: UsbInterface,
isdrAid: ByteArray isdrAid: ByteArray
): EuiccChannel? { ): 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 { try {
return EuiccChannelImpl( return EuiccChannelImpl(
context.getString(R.string.usb), context.getString(R.string.usb),
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
intrinsicChannelName = usbDevice.productName, intrinsicChannelName = ccidCtx.productName,
UsbApduInterface( UsbApduInterface(
conn, ccidCtx
bulkIn,
bulkOut,
context.preferenceRepository.verboseLoggingFlow
), ),
isdrAid, isdrAid,
context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.verboseLoggingFlow,

View file

@ -5,6 +5,7 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.usb.UsbCcidContext
import im.angry.openeuicc.core.usb.smartCard import im.angry.openeuicc.core.usb.smartCard
import im.angry.openeuicc.core.usb.interfaces import im.angry.openeuicc.core.usb.interfaces
import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.di.AppContainer
@ -275,11 +276,15 @@ open class DefaultEuiccChannelManager(
TAG, TAG,
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel" "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 { try {
val channel = tryOpenChannelFirstValidAid { val channel = tryOpenChannelFirstValidAid {
euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface, it) euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
} }
if (channel != null && channel.lpa.valid) { if (channel != null && channel.lpa.valid) {
ccidCtx.allowDisconnect = true
usbChannel = channel usbChannel = channel
return@withContext Pair(device, true) return@withContext Pair(device, true)
} }
@ -287,6 +292,10 @@ open class DefaultEuiccChannelManager(
// Ignored -- skip forward // Ignored -- skip forward
e.printStackTrace() e.printStackTrace()
} }
ccidCtx.allowDisconnect = true
ccidCtx.disconnect()
Log.i( Log.i(
TAG, TAG,
"No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}" "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"

View file

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

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.hardware.usb.UsbDevice import im.angry.openeuicc.core.usb.UsbCcidContext
import android.hardware.usb.UsbInterface
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just // 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? suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
fun tryOpenUsbEuiccChannel( fun tryOpenUsbEuiccChannel(
usbDevice: UsbDevice, ccidCtx: UsbCcidContext,
usbInterface: UsbInterface,
isdrAid: ByteArray isdrAid: ByteArray
): EuiccChannel? ): EuiccChannel?

View file

@ -13,7 +13,7 @@ class EuiccChannelImpl(
override val port: UiccPortInfoCompat, override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?, override val intrinsicChannelName: String?,
override val apduInterface: ApduInterface, override val apduInterface: ApduInterface,
isdrAid: ByteArray, override val isdrAid: ByteArray,
verboseLoggingFlow: Flow<Boolean>, verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean> ignoreTLSCertificateFlow: Flow<Boolean>
) : EuiccChannel { ) : EuiccChannel {

View file

@ -38,6 +38,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.apduInterface get() = channel.apduInterface
override val atr: ByteArray? override val atr: ByteArray?
get() = channel.atr get() = channel.atr
override val isdrAid: ByteArray
get() = channel.isdrAid
override fun close() = channel.close() override fun close() = channel.close()

View file

@ -1,27 +1,19 @@
package im.angry.openeuicc.core.usb package im.angry.openeuicc.core.usb
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.ApduInterfaceAtrProvider import im.angry.openeuicc.core.ApduInterfaceAtrProvider
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
class UsbApduInterface( class UsbApduInterface(
private val conn: UsbDeviceConnection, private val ccidCtx: UsbCcidContext
private val bulkIn: UsbEndpoint,
private val bulkOut: UsbEndpoint,
private val verboseLoggingFlow: Flow<Boolean>
) : ApduInterface, ApduInterfaceAtrProvider { ) : ApduInterface, ApduInterfaceAtrProvider {
companion object { companion object {
private const val TAG = "UsbApduInterface" private const val TAG = "UsbApduInterface"
} }
private lateinit var ccidDescription: UsbCcidDescription override val atr: ByteArray?
private lateinit var transceiver: UsbCcidTransceiver get() = ccidCtx.atr
override var atr: ByteArray? = null
override val valid: Boolean override val valid: Boolean
get() = channels.isNotEmpty() get() = channels.isNotEmpty()
@ -29,22 +21,7 @@ class UsbApduInterface(
private var channels = mutableSetOf<Int>() private var channels = mutableSetOf<Int>()
override fun connect() { override fun connect() {
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!! ccidCtx.connect()
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 // Send Terminal Capabilities
// Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
@ -56,11 +33,7 @@ class UsbApduInterface(
transmitApduByChannel(terminalCapabilities, 0) transmitApduByChannel(terminalCapabilities, 0)
} }
override fun disconnect() { override fun disconnect() = ccidCtx.disconnect()
conn.close()
atr = null
}
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
// OPEN LOGICAL CHANNEL // OPEN LOGICAL CHANNEL
@ -149,7 +122,7 @@ class UsbApduInterface(
// OR the channel mask into the CLA byte // OR the channel mask into the CLA byte
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte() 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)!") if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
@ -160,7 +133,7 @@ class UsbApduInterface(
// 0x6C = wrong le // 0x6C = wrong le
// so we fix the le field here // so we fix the le field here
realTx[realTx.size - 1] = resp[resp.size - 1] realTx[realTx.size - 1] = resp[resp.size - 1]
resp = transceiver.sendXfrBlock(realTx).data!! resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
} else if (sw1 == 0x61) { } else if (sw1 == 0x61) {
// 0x61 = X bytes available // 0x61 = X bytes available
// continue reading by GET RESPONSE // continue reading by GET RESPONSE
@ -170,7 +143,7 @@ class UsbApduInterface(
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte() 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 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.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -517,8 +516,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_euicc_memory_reset_failure), getString(R.string.task_euicc_memory_reset_failure),
R.drawable.ic_euicc_memory_reset R.drawable.ic_euicc_memory_reset
) { ) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.beginTrackedOperation(slotId, portId) {
channel.lpa.euiccMemoryReset() 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_access_mode, channel.type))
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) 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_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 -> channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) } 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)) } 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 true
} }
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View file

@ -1,11 +1,9 @@
package im.angry.openeuicc.ui.wizard package im.angry.openeuicc.ui.wizard
import android.os.Bundle import android.os.Bundle
import android.util.Patterns
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
@ -86,10 +84,34 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
} }
private fun updateInputCompleteness() { private fun updateInputCompleteness() {
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches() inputComplete = isValidAddress(smdp.editText!!.text)
if (state.confirmationCodeRequired) { if (state.confirmationCodeRequired) {
inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty() inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
} }
refreshButtons() 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_bl_ver">Product Bootloader Version</string>
<string name="euicc_info_fw_ver">Product Firmware 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_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_sgp22_version">SGP.22 Version</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string> <string name="euicc_info_firmware_version">eUICC OS Version</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string> <string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>