ソースを参照

feat: detect used product (#147)

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/147
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
septs 11 ヶ月 前
コミット
1d67fa5cfa

+ 1 - 0
.idea/.gitignore

@@ -9,5 +9,6 @@
 /navEditor.xml
 /navEditor.xml
 /runConfigurations.xml
 /runConfigurations.xml
 /workspace.xml
 /workspace.xml
+/AndroidProjectSystem.xml
 
 
 **/*.iml
 **/*.iml

+ 19 - 24
app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt

@@ -23,6 +23,8 @@ import im.angry.openeuicc.common.R
 import im.angry.openeuicc.core.EuiccChannel
 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 im.angry.openeuicc.vendored.getESTKmeInfo
+import im.angry.openeuicc.vendored.getSIMLinkVersion
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
 import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
 import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
 import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
@@ -100,24 +102,22 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
 
     private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
     private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
         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_eid,
-                channel.lpa.eID,
-                copiedToastResId = R.string.toast_eid_copied
-            )
-        )
+        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))
+        getESTKmeInfo(channel.apduInterface)?.let {
+            add(Item(R.string.euicc_info_sku, it.skuName))
+            add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied))
+            add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion))
+            add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
+        }
+        getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
+            add(Item(R.string.euicc_info_sku, "9eSIM $it"))
+        }
         channel.lpa.euiccInfo2.let { info ->
         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_sas_accreditation_number, info?.sasAccreditationNumber))
             add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
             add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
         }
         }
@@ -134,13 +134,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
             }
             add(Item(R.string.euicc_info_ci_type, getString(resId)))
             add(Item(R.string.euicc_info_ci_type, getString(resId)))
         }
         }
-        add(
-            Item(
-                R.string.euicc_info_atr,
-                channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
-                copiedToastResId = R.string.toast_atr_copied,
-            )
-        )
+        val atr =  channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
+        add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
     }
     }
 
 
     private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
     private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =

+ 49 - 0
app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt

@@ -0,0 +1,49 @@
+package im.angry.openeuicc.vendored
+
+import android.util.Log
+import im.angry.openeuicc.core.ApduInterfaceAtrProvider
+import im.angry.openeuicc.util.TAG
+import im.angry.openeuicc.util.decodeHex
+import net.typeblog.lpac_jni.ApduInterface
+
+data class ESTKmeInfo(
+    val serialNumber: String?,
+    val bootloaderVersion: String?,
+    val firmwareVersion: String?,
+    val skuName: String?,
+)
+
+fun isESTKmeATR(iface: ApduInterface): Boolean {
+    if (iface !is ApduInterfaceAtrProvider) return false
+    val atr = iface.atr ?: return false
+    val fpr = "estk.me".encodeToByteArray()
+    for (index in atr.indices) {
+        if (atr.size - index < fpr.size) break
+        if (atr.sliceArray(index until index + fpr.size).contentEquals(fpr)) return true
+    }
+    return false
+}
+
+fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? {
+    if (!isESTKmeATR(iface)) return null
+    fun decode(b: ByteArray): String? {
+        if (b.size < 2) return null
+        if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
+        return b.sliceArray(0 until b.size - 2).decodeToString()
+    }
+    return try {
+        iface.withLogicalChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { transmit ->
+            fun invoke(p1: Byte) = decode(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)))
+            ESTKmeInfo(
+                invoke(0x00), // serial number
+                invoke(0x01), // bootloader version
+                invoke(0x02), // firmware version
+                invoke(0x03), // sku name
+            )
+        }
+    } catch (e: Exception) {
+        Log.d(TAG, "Failed to get ESTKmeInfo", e)
+        null
+    }
+}
+

+ 20 - 0
app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt

@@ -0,0 +1,20 @@
+package im.angry.openeuicc.vendored
+
+import net.typeblog.lpac_jni.Version
+
+private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix
+
+fun getSIMLinkVersion(eid: String, version: Version?): String? {
+    if (version == null || prefix.find(eid, 0) == null) return null
+    return when {
+        // @formatter:off
+        version >= Version(37,  1, 41) -> "v3.1 (beta 1)"
+        version >= Version(36, 18,  5) -> "v3 (final)"
+        version >= Version(36, 17, 39) -> "v3 (beta)"
+        version >= Version(36, 17,  4) -> "v2s"
+        version >= Version(36,  9,  3) -> "v2.1"
+        version >= Version(36,  7,  2) -> "v2"
+        // @formatter:on
+        else -> null
+    }
+}

+ 5 - 0
app-common/src/main/res/values/strings.xml

@@ -31,6 +31,7 @@
     <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
     <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
     <string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
     <string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
     <string name="toast_iccid_copied">ICCID copied to clipboard</string>
     <string name="toast_iccid_copied">ICCID copied to clipboard</string>
+    <string name="toast_sn_copied">Serial Number copied to clipboard</string>
     <string name="toast_eid_copied">EID copied to clipboard</string>
     <string name="toast_eid_copied">EID copied to clipboard</string>
     <string name="toast_atr_copied">ATR copied to clipboard</string>
     <string name="toast_atr_copied">ATR copied to clipboard</string>
 
 
@@ -125,6 +126,10 @@
     <string name="euicc_info_activity_title">eUICC Info (%s)</string>
     <string name="euicc_info_activity_title">eUICC Info (%s)</string>
     <string name="euicc_info_access_mode">Access Mode</string>
     <string name="euicc_info_access_mode">Access Mode</string>
     <string name="euicc_info_removable">Removable</string>
     <string name="euicc_info_removable">Removable</string>
+    <string name="euicc_info_sku">Product Name</string>
+    <string name="euicc_info_sn">Product Serial Number</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_eid" translatable="false">EID</string>
     <string name="euicc_info_eid" translatable="false">EID</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>

+ 25 - 8
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 */
 /* Corresponds to EuiccInfo2 in SGP.22 */
 data class EuiccInfo2(
 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 sasAccreditationNumber: String,
-    val ppVersion: String,
+    val ppVersion: Version,
     val freeNvram: Int,
     val freeNvram: Int,
     val freeRam: 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))
+    private 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"
+}

+ 22 - 23
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.LocalProfileInfo
 import net.typeblog.lpac_jni.LocalProfileNotification
 import net.typeblog.lpac_jni.LocalProfileNotification
 import net.typeblog.lpac_jni.ProfileDownloadCallback
 import net.typeblog.lpac_jni.ProfileDownloadCallback
+import net.typeblog.lpac_jni.Version
 
 
 class LocalProfileAssistantImpl(
 class LocalProfileAssistantImpl(
     isdrAid: ByteArray,
     isdrAid: ByteArray,
@@ -84,8 +85,8 @@ class LocalProfileAssistantImpl(
             throw IllegalArgumentException("Failed to initialize LPA")
             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) {
     override fun setEs10xMss(mss: Byte) {
@@ -157,31 +158,29 @@ class LocalProfileAssistantImpl(
             val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
             val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
             if (cInfo == 0L) return null
             if (cInfo == 0L) return null
 
 
-            val euiccCiPKIdListForSigning = mutableListOf<String>()
-            var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
-            while (curr != 0L) {
-                euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr))
-                curr = LpacJni.stringArrNext(curr)
-            }
-
-            val euiccCiPKIdListForVerification = mutableListOf<String>()
-            curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
-            while (curr != 0L) {
-                euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr))
-                curr = LpacJni.stringArrNext(curr)
-            }
-
             val ret = EuiccInfo2(
             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.euiccInfo2GetSasAcreditationNumber(cInfo),
-                LpacJni.euiccInfo2GetPpVersion(cInfo),
+                Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
                 LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
                 LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
                 LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
                 LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
-                euiccCiPKIdListForSigning.toTypedArray(),
-                euiccCiPKIdListForVerification.toTypedArray()
+                buildSet {
+                    var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
+                    while (cursor != 0L) {
+                        add(LpacJni.stringDeref(cursor))
+                        cursor = LpacJni.stringArrNext(cursor)
+                    }
+                },
+                buildSet {
+                    var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
+                    while (cursor != 0L) {
+                        add(LpacJni.stringDeref(cursor))
+                        cursor = LpacJni.stringArrNext(cursor)
+                    }
+                },
             )
             )
 
 
             LpacJni.euiccInfo2Free(cInfo)
             LpacJni.euiccInfo2Free(cInfo)

+ 2 - 2
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt

@@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795
 
 
 // List of GSMA Live CIs
 // List of GSMA Live CIs
 // https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
 // https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
-val PKID_GSMA_LIVE_CI = arrayOf(
+val PKID_GSMA_LIVE_CI = setOf(
     // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
     // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
     // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
     // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
     DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
     DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
@@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = arrayOf(
 
 
 // SGP.26 v3.0, 2023-12-01
 // SGP.26 v3.0, 2023-12-01
 // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf
 // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf
-val PKID_GSMA_TEST_CI = arrayOf(
+val PKID_GSMA_TEST_CI = setOf(
     // Test CI (SGP.26, NIST P256)
     // Test CI (SGP.26, NIST P256)
     // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
     // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
     "34eecf13156518d48d30bdf06853404d115f955d",
     "34eecf13156518d48d30bdf06853404d115f955d",