feat: detect use product

This commit is contained in:
septs 2025-02-25 10:53:19 +08:00
parent 03bfdf373c
commit 1fcbeb0865
Signed by: septs
SSH key fingerprint: SHA256:ElK0p6DNkbsqYUdJ3I9QHDVf21SQD0c2r+hd7s/r5Co
14 changed files with 171 additions and 61 deletions

View file

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

View file

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

View file

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

View file

@ -83,4 +83,10 @@ class OmapiApduInterface(
throw e
}
}
override fun clone() = OmapiApduInterface(
service,
port,
verboseLoggingFlow,
)
}

View file

@ -169,4 +169,11 @@ class UsbApduInterface(
return resp
}
override fun clone() = UsbApduInterface(
conn,
bulkIn,
bulkOut,
verboseLoggingFlow,
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -125,6 +125,10 @@
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
<string name="euicc_info_access_mode">Access Mode</string>
<string name="euicc_info_removable">Removable</string>
<string name="euicc_info_sku">Product Name</string>
<string name="euicc_info_sn">Serial Number</string>
<string name="euicc_info_fw_ver">Firmware Number</string>
<string name="euicc_info_bl_ver">Bootloader Number</string>
<string name="euicc_info_eid" translatable="false">EID</string>
<string name="euicc_info_sgp22_version">SGP.22 Version</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string>

View file

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

View file

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

View file

@ -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<String>,
val euiccCiPKIdListForVerification: Array<String>,
)
val euiccCiPKIdListForSigning: Set<String>,
val euiccCiPKIdListForVerification: Set<String>,
)
data class Version(
val major: Int,
val minor: Int,
val patch: Int,
) {
constructor(version: String) : this(version.split('.').map(String::toInt))
constructor(parts: List<Int>) : 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"
}

View file

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