diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index f53e6ff..a05ba7e 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -30,7 +30,24 @@
+ android:label="@string/download_wizard">
+
+
+
+
+
+
+
+
+
+
+
+
+
= 0) {
isIndeterminate = false
@@ -289,7 +298,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
}
protected fun refreshButtons() {
- (requireActivity() as DownloadWizardActivity).refreshButtons()
+ requireWizardActivity().refreshButtons()
}
open fun beforeNext() {}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
index 5fa8002..1e6045d 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
@@ -35,8 +35,12 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
DownloadWizardProgressFragment()
- override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
- DownloadWizardMethodSelectFragment()
+ override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment {
+ if (requireWizardActivity().getActivationCodeFromIntent() != null) {
+ return DownloadWizardSlotSelectFragment()
+ }
+ return DownloadWizardMethodSelectFragment()
+ }
override fun onCreateView(
inflater: LayoutInflater,
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
index 2846fd7..c492f03 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
@@ -4,6 +4,7 @@ import android.app.AlertDialog
import android.content.ClipboardManager
import android.graphics.BitmapFactory
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -21,10 +22,16 @@ import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
+ companion object {
+ const val TAG = "DownloadWizardMethodSelectFragment"
+ }
+
data class DownloadMethod(
val iconRes: Int,
val titleRes: Int,
@@ -89,6 +96,11 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
DownloadWizardSlotSelectFragment()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requireWizardActivity().getActivationCodeFromIntent()?.let(::processLpaString)
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -125,28 +137,30 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
}
private fun processLpaString(input: String) {
+ if (runBlocking { preferenceRepository.verboseLoggingFlow.first() }) {
+ Log.i(TAG, "Processing LPA string: $input")
+ }
try {
val parsed = ActivationCode.fromString(input)
state.smdp = parsed.address
state.matchingId = parsed.matchingId
if (parsed.confirmationCodeRequired) {
- AlertDialog.Builder(requireContext()).apply {
- setTitle(R.string.profile_download_required_confirmation_code)
- setMessage(R.string.profile_download_required_confirmation_code_message)
- setCancelable(true)
- setPositiveButton(android.R.string.ok, null)
- show()
- }
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.profile_download_required_confirmation_code)
+ .setMessage(R.string.profile_download_required_confirmation_code_message)
+ .setCancelable(true)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
}
gotoNextFragment(DownloadWizardDetailsFragment())
- } catch (e: IllegalArgumentException) {
- AlertDialog.Builder(requireContext()).apply {
- setTitle(R.string.profile_download_incorrect_lpa_string)
- setMessage(R.string.profile_download_incorrect_lpa_string_message)
- setCancelable(true)
- setNegativeButton(android.R.string.cancel, null)
- show()
- }
+ } catch (e: IllegalStateException) {
+ Log.d(TAG, "Failed to parse LPA string", e)
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.profile_download_incorrect_lpa_string)
+ .setMessage(R.string.profile_download_incorrect_lpa_string_message)
+ .setCancelable(true)
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt
index c21e837..fbecfae 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt
@@ -7,20 +7,26 @@ data class ActivationCode(
val confirmationCodeRequired: Boolean = false,
) {
companion object {
- fun fromString(input: String): ActivationCode {
- val components = input.removePrefix("LPA:").split('$')
- if (components.size < 2 || components[0] != "1") {
- throw IllegalArgumentException("Invalid activation code format")
- }
+ fun fromString(token: String): ActivationCode {
+ val input = if (token.startsWith("LPA:", true)) token.drop(4) else token
+ val components = input.split('$').map { it.trim().ifEmpty { null } }
+ check(components.size >= 2) { "Invalid activation code format" }
+ check(components[0] == "1") { "Invalid activation code version" }
return ActivationCode(
- address = components[1].trim(),
- matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
- oid = components.getOrNull(3)?.trim()?.ifBlank { null },
- confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
+ checkNotNull(components[1]) { "Invalid SM-DP+ address" },
+ components.getOrNull(2),
+ components.getOrNull(3),
+ components.getOrNull(4) == "1",
)
}
}
+ init {
+ check(isFQDN(address)) { "Invalid SM-DP+ address" }
+ check(matchingId == null || isMatchingID(matchingId)) { "Invalid Matching ID" }
+ check(oid == null || isObjectIdentifier(oid)) { "Invalid OID" }
+ }
+
override fun toString(): String {
val parts = arrayOf(
"1",
@@ -31,4 +37,40 @@ data class ActivationCode(
)
return parts.joinToString("$").trimEnd('$')
}
-}
\ No newline at end of file
+}
+
+/**
+ * SGP.22 4.1 Activation Code (v2.2.2, p111)
+ *
+ * FQDN (Fully Qualified Domain Name) of the SM-DP+ (e.g., SMDP.GSMA.COM)
+ * restricted to the Alphanumeric mode character set defined in table 5 of ISO/IEC 18004 [15]
+ * excluding '$'
+ */
+private fun isFQDN(input: CharSequence): Boolean {
+ if (input.isEmpty() || input.length > 255) return false
+ val parts = input.split('.')
+ if (parts.size < 2) return false
+ for (part in parts) {
+ if (part.isEmpty() || part.length > 63) return false
+ if (part.all { it.isLetterOrDigit() || it == '-' }) continue
+ return false
+ }
+ return true
+}
+
+/**
+ * SGP.22 4.1.1 Matching ID (v2.2.2, p112)
+ *
+ * Matching ID is a string of alphanumeric characters and hyphens.
+ */
+private fun isMatchingID(input: CharSequence) =
+ input.all { it.isLetterOrDigit() || it == '-' }
+
+/**
+ * SGP.22 4.1 Activation Code (v2.2.2, p111)
+ *
+ * SM-DP+ OID in the CERT.DPauth.ECDSA
+ */
+private fun isObjectIdentifier(input: CharSequence) = input.length < 255 &&
+ input.count { it == '.' } > 2 &&
+ input.split('.').all { it.isNotEmpty() && it.all(Char::isDigit) }