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 0a7e16c..84b300e 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
@@ -19,13 +19,12 @@ import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch
-import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
+import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
class EuiccInfoActivity : BaseEuiccAccessActivity() {
companion object {
private val YES_NO = Pair(R.string.yes, R.string.no)
- private val SUPPORTED_UNSUPPORTED = Pair(R.string.supported, R.string.unsupported)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
@@ -107,21 +106,17 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
- add(
- Pair(
- R.string.euicc_info_gsma_prod,
- formatByBoolean(
- signers.contains(DEFAULT_PKID_GSMA_RSP2_ROOT_CI1),
- SUPPORTED_UNSUPPORTED
- )
- )
- )
- add(
- Pair(
- R.string.euicc_info_gsma_test,
- formatByBoolean(PKID_GSMA_TEST_CI.any(signers::contains), SUPPORTED_UNSUPPORTED)
- )
- )
+ // SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
+ // https://www.gsma.com/newsroom/wp-content/uploads/SGP.28-v1.0.pdf#page=5
+ // FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
+ // https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
+ val resId = when {
+ signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common
+ PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
+ PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
+ else -> R.string.euicc_info_ci_unknown
+ }
+ add(Pair(R.string.euicc_info_ci_type, getString(resId)))
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
index a06b587..7f82f22 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
@@ -4,6 +4,7 @@ import android.app.Dialog
import android.os.Bundle
import android.text.Editable
import android.widget.EditText
+import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
@@ -16,31 +17,47 @@ import kotlinx.coroutines.launch
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
companion object {
const val TAG = "ProfileDeleteFragment"
+ private const val FIELD_ICCID = "iccid"
+ private const val FIELD_NAME = "name"
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
instance.requireArguments().apply {
- putString("iccid", iccid)
- putString("name", name)
+ putString(FIELD_ICCID, iccid)
+ putString(FIELD_NAME, name)
}
return instance
}
}
+ private val iccid by lazy {
+ requireArguments().getString(FIELD_ICCID)!!
+ }
+
+ private val name by lazy {
+ requireArguments().getString(FIELD_NAME)!!
+ }
+
private val editText by lazy {
EditText(requireContext()).apply {
- hint = Editable.Factory.getInstance().newEditable(
- getString(R.string.profile_delete_confirm_input, requireArguments().getString("name")!!)
- )
+ hint = Editable.Factory.getInstance()
+ .newEditable(getString(R.string.profile_delete_confirm_input, name))
}
}
+
private val inputMatchesName: Boolean
- get() = editText.text.toString() == requireArguments().getString("name")!!
+ get() = editText.text.toString() == name
+
+ private var toast: Toast? = null
+
private var deleting = false
+ private val alertDialog: AlertDialog
+ get() = requireDialog() as AlertDialog
+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
- setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name")))
+ setMessage(getString(R.string.profile_delete_confirm, name))
setView(editText)
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
setNegativeButton(android.R.string.cancel, null)
@@ -48,9 +65,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
override fun onResume() {
super.onResume()
- val alertDialog = dialog!! as AlertDialog
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
- if (!deleting && inputMatchesName) delete()
+ if (!deleting) delete()
}
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
if (!deleting) dismiss()
@@ -58,8 +74,15 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
}
private fun delete() {
+ toast?.cancel()
+ if (!inputMatchesName) {
+ val resId = R.string.toast_profile_delete_confirm_text_mismatched
+ toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).also {
+ it.show()
+ }
+ return
+ }
deleting = true
- val alertDialog = dialog!! as AlertDialog
alertDialog.setCanceledOnTouchOutside(false)
alertDialog.setCancelable(false)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
@@ -68,12 +91,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
-
- euiccChannelManagerService.launchProfileDeleteTask(
- slotId,
- portId,
- requireArguments().getString("iccid")!!
- ).onStart {
+ euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
if (parentFragment is EuiccProfilesChangedListener) {
// Trigger a refresh in the parent fragment -- it should wait until
// any foreground task is completed before actually doing a refresh
diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml
index f308826..61823f7 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
Cannot switch to new eSIM profile.
Nickname cannot be longer than 64 characters
+ Confirmation string mismatch
ICCID copied to clipboard
Select Slot
@@ -122,11 +123,10 @@
SAS Accreditation Number
Protected Profile Version
Free NVRAM (eSIM profile storage)
- GSMA Production Certificate
- GSMA Test Certificate
-
- Supported
- Unsupported
+ Certificate Issuer
+ GSMA Live CI
+ GSMA Test CI
+ Unknown eSIM CA
Yes
No
diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt
index 82c443f..cfd5779 100644
--- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt
+++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt
@@ -7,8 +7,32 @@ import java.security.cert.CertificateFactory
const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb"
-val PKID_GSMA_TEST_CI =
- arrayOf("34eecf13156518d48d30bdf06853404d115f955d", "2209f61cd9ec5c9c854e787341ff83ecf9776a5b")
+// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
+// https://www.gsma.com/newsroom/wp-content/uploads/SGP.28-v1.0.pdf#page=5
+// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
+// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
+
+// List of GSMA Live CIs
+// https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
+val PKID_GSMA_LIVE_CI = arrayOf(
+ // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
+ // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
+ DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
+ // OISITE GSMA CI G1 (SGP.22 v2+v3, CA: WISeKey)
+ // https://euicc-manual.osmocom.org/docs/pki/ci/files/4c2796.txt
+ "4c27967ad20c14b391e9601e41e604ad57c0222f",
+)
+
+// 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
+val PKID_GSMA_TEST_CI = arrayOf(
+ // Test CI (SGP.26, NIST P256)
+ // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
+ "34eecf13156518d48d30bdf06853404d115f955d",
+ // Test CI (SGP.26, BRP P256r1)
+ // https://euicc-manual.osmocom.org/docs/pki/ci/files/2209f6.txt
+ "2209f61cd9ec5c9c854e787341ff83ecf9776a5b",
+)
private fun getCertificate(keyId: String): Certificate? =
KNOWN_CI_CERTS[keyId]?.toByteArray()?.let { cert ->
@@ -60,7 +84,7 @@ internal val KNOWN_CI_CERTS = hashMapOf(
-----END CERTIFICATE-----
""".trimIndent(),
// OISITE GSMA CI G1 (CA: WISeKey)
- // Specs: SGP.21 and SGP.22 version 3
+ // Specs: SGP.21 and SGP.22 version 2 and version 3
"4c27967ad20c14b391e9601e41e604ad57c0222f" to """
-----BEGIN CERTIFICATE-----
MIIB9zCCAZ2gAwIBAgIUSpBSCCDYPOEG/IFHUCKpZ2pIAQMwCgYIKoZIzj0EAwIw