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 5f399ea..cf8b6d0 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,6 +1,8 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
+import im.angry.openeuicc.vendored.ESTKmeInfo
+import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
interface EuiccChannel {
@@ -28,5 +30,7 @@ interface EuiccChannel {
*/
val intrinsicChannelName: String?
+ fun forkApduInterface(): 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 3da829a..f49ac6f 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,6 +1,8 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
+import im.angry.openeuicc.vendored.ESTKmeInfo
+import im.angry.openeuicc.vendored.getESTKmeInfo
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
@@ -34,6 +36,8 @@ class EuiccChannelImpl(
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
+ override fun forkApduInterface() = apduInterface.clone()
+
override val valid: Boolean
get() = lpa.valid
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 4204e82..1a108e5 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,9 +1,8 @@
package im.angry.openeuicc.core
-import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant
-class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
+class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel by orig {
private var _inner: EuiccChannel? = orig
private val channel: EuiccChannel
@@ -15,28 +14,11 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
return _inner!!
}
- override val type: String
- get() = channel.type
- override val port: UiccPortInfoCompat
- get() = channel.port
- override val slotId: Int
- get() = channel.slotId
- override val logicalSlotId: Int
- get() = channel.logicalSlotId
- override val portId: Int
- get() = channel.portId
private val lpaDelegate = lazy {
LocalProfileAssistantWrapper(channel.lpa)
}
- override val lpa: LocalProfileAssistant by lpaDelegate
- override val valid: Boolean
- get() = channel.valid
- override val intrinsicChannelName: String?
- get() = channel.intrinsicChannelName
- override val atr: ByteArray?
- get() = channel.atr
- override fun close() = channel.close()
+ override val lpa: LocalProfileAssistant by lpaDelegate
fun invalidateWrapper() {
_inner = null
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 c70669d..7fd7188 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
@@ -83,4 +83,10 @@ class OmapiApduInterface(
throw e
}
}
+
+ override fun clone() = OmapiApduInterface(
+ service,
+ port,
+ verboseLoggingFlow,
+ )
}
\ No newline at end of file
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 624ef89..8bb6470 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
@@ -169,4 +169,11 @@ class UsbApduInterface(
return resp
}
+
+ override fun clone() = UsbApduInterface(
+ conn,
+ bulkIn,
+ bulkOut,
+ verboseLoggingFlow,
+ )
}
\ 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 e88ad01..a2ac085 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,6 +23,9 @@ 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.ESTKmeInfo
+import im.angry.openeuicc.vendored.getESTKmeInfo
+import im.angry.openeuicc.vendored.getNineVersion
import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
@@ -106,6 +109,15 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
formatByBoolean(channel.port.card.isRemovable, YES_NO)
)
)
+ getESTKmeInfo(channel.forkApduInterface())?.let {
+ add(Item(R.string.euicc_info_sku, it.skuName))
+ add(Item(R.string.euicc_info_sn, it.serialNumber))
+ add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion))
+ add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
+ }
+ getNineVersion(channel.lpa.eID, channel.lpa.euiccInfo2)?.let {
+ add(Item(R.string.euicc_info_sku, "9eSIM $it"))
+ }
add(
Item(
R.string.euicc_info_eid,
@@ -114,10 +126,15 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
)
)
channel.lpa.euiccInfo2.let { info ->
- 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_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_sas_accreditation_number, info?.sasAccreditationNumber))
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
index 8d72462..f252d76 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
@@ -12,13 +12,10 @@ fun String.decodeHex(): ByteArray {
return out
}
-fun ByteArray.encodeHex(): String {
- val sb = StringBuilder()
- val length = size
- for (i in 0 until length) {
- sb.append(String.format("%02X", this[i]))
+fun ByteArray.encodeHex(): String = buildString {
+ for (element in this@encodeHex) {
+ append(String.format("%02X", element))
}
- return sb.toString()
}
fun formatFreeSpace(size: Int): String =
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
new file mode 100644
index 0000000..8c06706
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt
@@ -0,0 +1,46 @@
+package im.angry.openeuicc.vendored
+
+import android.util.Log
+import net.typeblog.lpac_jni.ApduInterface
+
+data class ESTKmeInfo(
+ val serialNumber: String?,
+ val bootloaderVersion: String?,
+ val firmwareVersion: String?,
+ val skuName: String?,
+)
+
+fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? {
+ // @formatter:off
+ val aid = byteArrayOf(
+ 0xA0.toByte(), 0x65.toByte(), 0x73.toByte(), 0x74.toByte(), 0x6B.toByte(), 0x6D.toByte(),
+ 0x65.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(),
+ 0xFF.toByte(), 0x6D.toByte(), 0x67.toByte(), 0x74.toByte(),
+ )
+ // @formatter:on
+ return try {
+ iface.connect()
+ iface.logicalChannelOpen(aid)
+ fun transmit(p1: Byte) =
+ decode(iface.transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)).clone())
+ return ESTKmeInfo(
+ transmit(0x00), // serial number
+ transmit(0x01), // bootloader version
+ transmit(0x02), // firmware version
+ transmit(0x03), // sku name
+ )
+ } catch (e: Exception) {
+ Log.d("ESTKme", "Failed to get ESTKme info", e)
+ null
+ } finally {
+ iface.disconnect()
+ }
+}
+
+private fun isSuccessResponse(b: ByteArray) =
+ b.size >= 2 && b[b.size - 2] == 0x90.toByte() && b[b.size - 1] == 0x00.toByte()
+
+private fun decode(b: ByteArray): String? {
+ if (!isSuccessResponse(b)) return null
+ return b.dropLast(2).toByteArray().decodeToString()
+}
diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/nine.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/nine.kt
new file mode 100644
index 0000000..e0d2679
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/vendored/nine.kt
@@ -0,0 +1,22 @@
+package im.angry.openeuicc.vendored
+
+import net.typeblog.lpac_jni.EuiccInfo2
+import net.typeblog.lpac_jni.Version
+
+private val prefix = Regex("^89044045(84|21)67274948")
+
+fun getNineVersion(eid: String, euiccInfo2: EuiccInfo2?): String? {
+ if (euiccInfo2 == null) return null
+ if (!prefix.matches(eid)) return null
+ val version = euiccInfo2.euiccFirmwareVersion
+ return when {
+ // @formatter:off
+ version >= Version(36, 7, 2) -> "v2"
+ version >= Version(36, 9, 3) -> "v2.1"
+ version >= Version(36, 17, 4) -> "v2s"
+ version >= Version(36, 17, 39) -> "v3 (beta)"
+ version >= Version(36, 18, 5) -> "v3"
+ // @formatter:on
+ else -> null
+ }
+}
diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml
index 71e2418..5a8063c 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -125,6 +125,10 @@
eUICC Info (%s)
Access Mode
Removable
+ Product Name
+ Serial Number
+ Firmware Number
+ Bootloader Number
EID
SGP.22 Version
eUICC OS Version
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 6b09368..265bd6a 100644
--- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt
+++ b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt
@@ -59,24 +59,26 @@ class TelephonyManagerApduInterface(
}
val cla = tx[0].toUByte().toInt()
- val instruction = tx[1].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 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()
+ // @formatter:off
+ val result = tm.iccTransmitApduLogicalChannelByPortCompat(
+ port.card.physicalSlotIndex, port.portIndex, lastChannel,
+ cla, ins, p1, p2, p3, p4
+ )
+ // @formatter:on
+ if (runBlocking { verboseLoggingFlow.first() })
+ Log.d(TAG, "TelephonyManager APDU response: $result")
+ return result?.decodeHex() ?: byteArrayOf()
}
+ override fun clone() = TelephonyManagerApduInterface(
+ port,
+ tm,
+ verboseLoggingFlow
+ )
}
\ 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 dfa92df..8075779 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
@@ -3,12 +3,13 @@ package net.typeblog.lpac_jni
/*
* Should reflect euicc_apdu_interface in lpac/euicc/interface.h
*/
-interface ApduInterface {
+interface ApduInterface : Cloneable {
fun connect()
fun disconnect()
fun logicalChannelOpen(aid: ByteArray): Int
fun logicalChannelClose(handle: Int)
fun transmit(tx: ByteArray): ByteArray
+ public override fun clone(): ApduInterface
/**
* Is this APDU connection still valid?
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 6c73051..2a9b3e5 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,14 +2,31 @@ package net.typeblog.lpac_jni
/* Corresponds to EuiccInfo2 in SGP.22 */
data class EuiccInfo2(
- val sgp22Version: String,
- val profileVersion: String,
- val euiccFirmwareVersion: String,
- val globalPlatformVersion: String,
+ val sgp22Version: Version,
+ val profileVersion: Version,
+ val euiccFirmwareVersion: Version,
+ val globalPlatformVersion: Version,
val sasAccreditationNumber: String,
- val ppVersion: String,
+ val ppVersion: Version,
val freeNvram: Int,
val freeRam: Int,
- val euiccCiPKIdListForSigning: Array,
- val euiccCiPKIdListForVerification: Array,
-)
\ No newline at end of file
+ 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))
+ 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"
+}
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 8aafe94..827a3b5 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,6 +10,7 @@ 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,
@@ -84,8 +85,8 @@ class LocalProfileAssistantImpl(
throw IllegalArgumentException("Failed to initialize LPA")
}
- val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf()
- httpInterface.usePublicKeyIds(pkids)
+ val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf()
+ httpInterface.usePublicKeyIds(pkids.toTypedArray())
}
override fun setEs10xMss(mss: Byte) {
@@ -172,16 +173,16 @@ class LocalProfileAssistantImpl(
}
val ret = EuiccInfo2(
- LpacJni.euiccInfo2GetSGP22Version(cInfo),
- LpacJni.euiccInfo2GetProfileVersion(cInfo),
- LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo),
- LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo),
+ Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
+ Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
- LpacJni.euiccInfo2GetPpVersion(cInfo),
+ Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
- euiccCiPKIdListForSigning.toTypedArray(),
- euiccCiPKIdListForVerification.toTypedArray()
+ euiccCiPKIdListForSigning.toTypedArray().toSet(),
+ euiccCiPKIdListForVerification.toTypedArray().toSet(),
)
LpacJni.euiccInfo2Free(cInfo)